Improve image optimization run-time and memory usage (#457)
continuous-integration/drone/push Build is passing Details

This PR aims to resolve the issues surrounding the optimize-images script, at least in the short term, in order to unblock other work.

**Problems:** Our optimize-images script was taking a very long time to run (~7 minutes in CI, when successful). This led to two problems:
1. It was near impossible to run the script locally.
2. CI jobs would often get killed on the optimize-images step.

**Solutions:**
1. Resize the images in `images/events` prior to optimizing them, similar to what is done for the images in `images/team`. This solution on its own reduced the run-time of the script to ~30 seconds locally on Amy's laptop, which is comparable to the run-time of the script back when it was originally written.
2. EDIT: Copy/resize/optimize the images in batches of 32 at a time. The reason why the CI job was being killed is because the script would run out of memory, however this change should resolve that while also keeping build times reasonable (~30 sec locally/when deploying on caffeine, ~3 min in CI). ~~As a temporary fix, this PR also replaces the images in `images/events` with their resized + optimized versions. (For some unknown reason, Solution 1 is not sufficient to solve Problem 2.)~~

This PR also adds some logging to the script so we can get a (slightly) better sense of where the script is getting stuck.

Related issue: #456

Co-authored-by: Amy <a258wang@uwaterloo.ca>
Reviewed-on: #457
Reviewed-by: Shahan Neda <snedadah@csclub.uwaterloo.ca>
This commit is contained in:
Amy Wang 2022-06-04 19:31:00 -04:00
parent e336bed2aa
commit 443925190e
1 changed files with 65 additions and 36 deletions

View File

@ -16,14 +16,16 @@ import { default as getImageDimensions } from "image-size";
const SOURCE_DIRECTORY = "images"; const SOURCE_DIRECTORY = "images";
const DESTINATION_DIRECTORY = path.join("public", "images"); const DESTINATION_DIRECTORY = path.join("public", "images");
// directory where Meet the Team headshots are stored, relative to the source directory // directories are relative to SOURCE_DIRECTORY
const TEAM_IMAGES_DIRECTORY = path.join("team", ""); const TEAM_IMAGES_DIRECTORY = path.join("team", "");
const EVENTS_IMAGES_DIRECTORY = path.join("events", "");
const IMAGE_MINIMUM_SIZE = 512; const IMAGE_MINIMUM_SIZE = 512;
const GET_ENCODER_FROM_EXTENSION: { [imageExtension: string]: string } = { const GET_ENCODER_FROM_EXTENSION: { [imageExtension: string]: string } = {
jpg: "mozjpeg", jpg: "mozjpeg",
jpeg: "mozjpeg", jpeg: "mozjpeg",
JPG: "mozjpeg",
png: "oxipng", png: "oxipng",
}; };
@ -35,6 +37,8 @@ const ENCODER_OPTIONS: { [encoder: string]: Record<string, unknown> } = {
void optimizeImages(); void optimizeImages();
export async function optimizeImages() { export async function optimizeImages() {
const startTime = Date.now();
const imagePaths = await getFilePathsInDirectory(SOURCE_DIRECTORY); const imagePaths = await getFilePathsInDirectory(SOURCE_DIRECTORY);
await fse.emptyDir(DESTINATION_DIRECTORY); await fse.emptyDir(DESTINATION_DIRECTORY);
@ -42,8 +46,14 @@ export async function optimizeImages() {
const numberOfWorkers = Math.min(cpus().length, 8); const numberOfWorkers = Math.min(cpus().length, 8);
const imagePool = new ImagePool(numberOfWorkers); const imagePool = new ImagePool(numberOfWorkers);
// process smaller batches in order to reduce memory usage
const batchSize = 32;
for (let i = 0; i < imagePaths.length; i += batchSize) {
await Promise.all( await Promise.all(
imagePaths.map(async (imagePath) => { imagePaths.slice(i, i + batchSize).map(async (imagePath) => {
const imageStartTime = Date.now();
const sourcePath = path.join(SOURCE_DIRECTORY, imagePath); const sourcePath = path.join(SOURCE_DIRECTORY, imagePath);
const destinationPath = path.join(DESTINATION_DIRECTORY, imagePath); const destinationPath = path.join(DESTINATION_DIRECTORY, imagePath);
const fileExtension = imagePath.split(".").pop() ?? ""; const fileExtension = imagePath.split(".").pop() ?? "";
@ -51,6 +61,9 @@ export async function optimizeImages() {
if (!encoder) { if (!encoder) {
await fse.copy(sourcePath, destinationPath); await fse.copy(sourcePath, destinationPath);
console.log(
`Copied ${imagePath} in ${getElapsedSeconds(imageStartTime)}s`
);
return; return;
} }
@ -61,7 +74,8 @@ export async function optimizeImages() {
await ingestedImage.decoded; await ingestedImage.decoded;
const shouldResize = const shouldResize =
imagePath.startsWith(TEAM_IMAGES_DIRECTORY) && (imagePath.startsWith(TEAM_IMAGES_DIRECTORY) ||
imagePath.startsWith(EVENTS_IMAGES_DIRECTORY)) &&
(width ?? 0) > IMAGE_MINIMUM_SIZE && (width ?? 0) > IMAGE_MINIMUM_SIZE &&
(height ?? 0) > IMAGE_MINIMUM_SIZE; (height ?? 0) > IMAGE_MINIMUM_SIZE;
@ -77,6 +91,10 @@ export async function optimizeImages() {
}; };
await ingestedImage.preprocess(preprocessOptions); await ingestedImage.preprocess(preprocessOptions);
console.log(
`Resized ${sourcePath} in ${getElapsedSeconds(imageStartTime)}s`
);
} }
const encodeOptions = { [encoder]: ENCODER_OPTIONS[encoder] }; const encodeOptions = { [encoder]: ENCODER_OPTIONS[encoder] };
@ -84,10 +102,17 @@ export async function optimizeImages() {
const encodedImage = await ingestedImage.encodedWith[encoder]; const encodedImage = await ingestedImage.encodedWith[encoder];
await fse.outputFile(destinationPath, encodedImage.binary); await fse.outputFile(destinationPath, encodedImage.binary);
console.log(
`Optimized ${sourcePath} in ${getElapsedSeconds(imageStartTime)}s`
);
}) })
); );
}
await imagePool.close(); await imagePool.close();
console.log(`TOTAL DURATION: ${getElapsedSeconds(startTime)}s`);
} }
async function getFilePathsInDirectory(directory: string): Promise<string[]> { async function getFilePathsInDirectory(directory: string): Promise<string[]> {
@ -105,3 +130,7 @@ async function getFilePathsInDirectory(directory: string): Promise<string[]> {
) )
).flat(); ).flat();
} }
function getElapsedSeconds(startTime: number) {
return (Date.now() - startTime) / 1000;
}