DEV: Move loadLibs to explicit install message in media-optimization-worker (#14707)

Previously, `loadLibs` was called inside the `optimize` function of
the media-optimization-worker, which meant that it could be hit
multiple times causing load errors (as seen in b69c2f7311)

This commit moves that call to a specific message handler (the `install` message)
for the service worker, and refactors the service for the media-optimization-worker
to wait for this installation to complete before continuing with processing
image optimizations.

This way, we know for sure based on promises and worker messages
that the worker is installed and has all required libraries
loaded before we continue on with attempting any processing. The
change made in b69c2f7311 is no
longer needed with this commit.
This commit is contained in:
Martin Brennan 2021-10-25 13:17:43 +10:00 committed by GitHub
parent 8c17f5b72c
commit 2461ed303c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 103 additions and 67 deletions

View File

@ -4,6 +4,22 @@ import { Promise } from "rsvp";
import { fileToImageData } from "discourse/lib/media-optimization-utils";
import { getAbsoluteURL, getURLWithCDN } from "discourse-common/lib/get-url";
/**
* This worker follows a particular promise/callback flow to ensure
* that the media-optimization-worker is installed and has its libraries
* loaded before optimizations can happen. The flow:
*
* 1. optimizeImage called
* 2. worker initialized and started
* 3. message handlers for worker registered
* 4. "install" message posted to worker
* 5. "installed" message received from worker
* 6. optimizeImage continues, posting "compress" message to worker
*
* When the worker is being installed, all other calls to optimizeImage
* will wait for the "installed" message to be handled before continuing
* with any image optimization work.
*/
export default class MediaOptimizationWorkerService extends Service {
appEvents = getOwner(this).lookup("service:app-events");
worker = null;
@ -11,35 +27,7 @@ export default class MediaOptimizationWorkerService extends Service {
currentComposerUploadData = null;
promiseResolvers = null;
startWorker() {
this.logIfDebug("Starting media-optimization-worker");
this.worker = new Worker(this.workerUrl); // TODO come up with a workaround for FF that lacks type: module support
}
stopWorker() {
if (this.worker) {
this.logIfDebug("Stopping media-optimization-worker...");
this.worker.terminate();
this.worker = null;
}
}
ensureAvailiableWorker() {
if (!this.worker) {
this.startWorker();
this.registerMessageHandler();
this.appEvents.on("composer:closed", this, "stopWorker");
}
}
logIfDebug(message) {
if (this.siteSettings.composer_media_optimization_debug_mode) {
// eslint-disable-next-line no-console
console.log(message);
}
}
optimizeImage(data, opts = {}) {
async optimizeImage(data, opts = {}) {
this.usingUppy = data.id && data.id.includes("uppy");
this.promiseResolvers = this.promiseResolvers || {};
this.stopWorkerOnError = opts.hasOwnProperty("stopWorkerOnError")
@ -57,7 +45,8 @@ export default class MediaOptimizationWorkerService extends Service {
) {
return this.usingUppy ? Promise.resolve() : data;
}
this.ensureAvailiableWorker();
await this.ensureAvailiableWorker();
return new Promise(async (resolve) => {
this.logIfDebug(`Transforming ${file.name}`);
@ -85,18 +74,6 @@ export default class MediaOptimizationWorkerService extends Service {
width: imageData.width,
height: imageData.height,
settings: {
mozjpeg_script: getURLWithCDN(
"/javascripts/squoosh/mozjpeg_enc.js"
),
mozjpeg_wasm: getURLWithCDN(
"/javascripts/squoosh/mozjpeg_enc.wasm"
),
resize_script: getURLWithCDN(
"/javascripts/squoosh/squoosh_resize.js"
),
resize_wasm: getURLWithCDN(
"/javascripts/squoosh/squoosh_resize_bg.wasm"
),
resize_threshold: this.siteSettings
.composer_media_optimization_image_resize_dimensions_threshold,
resize_target: this.siteSettings
@ -116,6 +93,54 @@ export default class MediaOptimizationWorkerService extends Service {
});
}
async ensureAvailiableWorker() {
if (this.worker && this.workerInstalled) {
return Promise.resolve();
}
if (this.installPromise) {
return this.installPromise;
}
return this.install();
}
async install() {
this.installPromise = new Promise((resolve) => {
this.afterInstalled = resolve;
this.logIfDebug("Installing worker.");
this.startWorker();
this.registerMessageHandler();
this.worker.postMessage({
type: "install",
settings: {
mozjpeg_script: getURLWithCDN("/javascripts/squoosh/mozjpeg_enc.js"),
mozjpeg_wasm: getURLWithCDN("/javascripts/squoosh/mozjpeg_enc.wasm"),
resize_script: getURLWithCDN(
"/javascripts/squoosh/squoosh_resize.js"
),
resize_wasm: getURLWithCDN(
"/javascripts/squoosh/squoosh_resize_bg.wasm"
),
},
});
this.appEvents.on("composer:closed", this, "stopWorker");
});
return this.installPromise;
}
startWorker() {
this.logIfDebug("Starting media-optimization-worker");
this.worker = new Worker(this.workerUrl); // TODO come up with a workaround for FF that lacks type: module support
}
stopWorker() {
if (this.worker) {
this.logIfDebug("Stopping media-optimization-worker...");
this.workerInstalled = false;
this.worker.terminate();
this.worker = null;
}
}
registerMessageHandler() {
this.worker.onmessage = (e) => {
switch (e.data.type) {
@ -153,9 +178,23 @@ export default class MediaOptimizationWorkerService extends Service {
);
}
break;
case "installed":
this.logIfDebug("Worker installed.");
this.workerInstalled = true;
this.afterInstalled();
this.afterInstalled = null;
this.installPromise = null;
break;
default:
this.logIfDebug(`Sorry, we are out of ${e}.`);
}
};
}
logIfDebug(message) {
if (this.siteSettings.composer_media_optimization_debug_mode) {
// eslint-disable-next-line no-console
console.log(message);
}
}
}

View File

@ -2,10 +2,10 @@ function resizeWithAspect(
input_width,
input_height,
target_width,
target_height,
target_height
) {
if (!target_width && !target_height) {
throw Error('Need to specify at least width or height when resizing');
throw Error("Need to specify at least width or height when resizing");
}
if (target_width && target_height) {
@ -33,9 +33,6 @@ function logIfDebug(message) {
}
async function optimize(imageData, fileName, width, height, settings) {
await loadLibs(settings);
const mozJpegDefaultOptions = {
quality: settings.encode_quality,
baseline: false,
@ -63,7 +60,11 @@ async function optimize(imageData, fileName, width, height, settings) {
// resize
if (width > settings.resize_threshold) {
try {
const target_dimensions = resizeWithAspect(width, height, settings.resize_target);
const target_dimensions = resizeWithAspect(
width,
height,
settings.resize_target
);
const resizeResult = self.codecs.resize(
new Uint8ClampedArray(imageData),
width, //in
@ -75,12 +76,12 @@ async function optimize(imageData, fileName, width, height, settings) {
settings.resize_linear_rgb
);
if (resizeResult[3] !== 255) {
throw "Image corrupted during resize. Falling back to the original for encode"
throw "Image corrupted during resize. Falling back to the original for encode";
}
maybeResized = new ImageData(
resizeResult,
target_dimensions.width,
target_dimensions.height,
target_dimensions.height
).data;
width = target_dimensions.width;
height = target_dimensions.height;
@ -102,12 +103,12 @@ async function optimize(imageData, fileName, width, height, settings) {
mozJpegDefaultOptions
);
const finalSize = result.byteLength
const finalSize = result.byteLength;
logIfDebug(`Worker post reencode file: ${finalSize}`);
logIfDebug(`Reduction: ${(initialSize / finalSize).toFixed(1)}x speedup`);
if (finalSize < 20000) {
throw "Final size suspciously small, discarding optimizations"
throw "Final size suspciously small, discarding optimizations";
}
let transferrable = Uint8Array.from(result).buffer; // decoded was allocated inside WASM so it **cannot** be transfered to another context, need to copy by value
@ -132,7 +133,7 @@ onmessage = async function (e) {
type: "file",
file: optimized,
fileName: e.data.fileName,
fileId: e.data.fileId
fileId: e.data.fileId,
},
[optimized]
);
@ -142,31 +143,27 @@ onmessage = async function (e) {
type: "error",
file: e.data.file,
fileName: e.data.fileName,
fileId: e.data.fileId
fileId: e.data.fileId,
});
}
break;
case "install":
await loadLibs(e.data.settings);
postMessage({ type: "installed" });
break;
default:
logIfDebug(`Sorry, we are out of ${e}.`);
}
};
async function loadLibs(settings){
async function loadLibs(settings) {
if (self.codecs) return;
if (!self.loadedMozJpeg) {
importScripts(settings.mozjpeg_script);
self.loadedMozJpeg = true;
}
if (!self.loadedResizeScript) {
importScripts(settings.resize_script);
self.loadedResizeScript = true;
}
importScripts(settings.mozjpeg_script);
importScripts(settings.resize_script);
let encoderModuleOverrides = {
locateFile: function(path, prefix) {
locateFile: function (path, prefix) {
// if it's a mem init file, use a custom dir
if (path.endsWith(".wasm")) return settings.mozjpeg_wasm;
// otherwise, use the default, the prefix (JS file's dir) + the path
@ -181,5 +178,5 @@ async function loadLibs(settings){
const { resize } = wasm_bindgen;
await wasm_bindgen(settings.resize_wasm);
self.codecs = {mozjpeg_enc: mozjpeg_enc_module, resize: resize};
self.codecs = { mozjpeg_enc: mozjpeg_enc_module, resize: resize };
}