FEATURE: Limit client side image compression on Safari to version >= 18 (#28373)
This PR limits this feature: On all devices: - Browsers with OffScreenCanvas support - Browsers with createImageBitmap On Apple Safari - At least version 18 It also adds a routine that terminates the worker after 5 uses on all devices to handle any WASM memory leaks. All this together fixes crashes that could occur on iPhones. It still leaves the feature disabled by default on iOS, which will be revisited after testing this changes.
This commit is contained in:
parent
fc1c5f6a8d
commit
fd57a64174
|
@ -8,14 +8,6 @@ export default {
|
|||
const capabilities = owner.lookup("service:capabilities");
|
||||
|
||||
if (siteSettings.composer_media_optimization_image_enabled) {
|
||||
// NOTE: There are various performance issues with the Canvas
|
||||
// in iOS Safari that are causing crashes when processing images
|
||||
// with spikes of over 100% CPU usage. The cause of this is unknown,
|
||||
// but profiling points to CanvasRenderingContext2D.getImageData()
|
||||
// and CanvasRenderingContext2D.drawImage().
|
||||
//
|
||||
// Until Safari makes some progress with OffscreenCanvas or other
|
||||
// alternatives we cannot support this workflow.
|
||||
if (
|
||||
capabilities.isIOS &&
|
||||
!siteSettings.composer_ios_media_optimisation_image_enabled
|
||||
|
@ -23,6 +15,22 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
// Restrict feature to browsers that support OffscreenCanvas
|
||||
if (typeof OffscreenCanvas === "undefined") {
|
||||
return;
|
||||
}
|
||||
if (!("createImageBitmap" in self)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// prior to v18, Safari has WASM memory growth bugs
|
||||
// eg https://github.com/emscripten-core/emscripten/issues/19144
|
||||
let match = window.navigator.userAgent.match(/Mobile\/([0-9]+)\./);
|
||||
let safariVersion = match ? parseInt(match[1], 10) : null;
|
||||
if (capabilities.isSafari && safariVersion && safariVersion < 18) {
|
||||
return;
|
||||
}
|
||||
|
||||
addComposerUploadPreProcessor(
|
||||
UppyMediaOptimization,
|
||||
({ isMobileDevice }) => {
|
||||
|
|
|
@ -1,35 +1,5 @@
|
|||
import { Promise } from "rsvp";
|
||||
import { helperContext } from "discourse-common/lib/helpers";
|
||||
|
||||
// Chrome and Firefox use a native method to do Image -> Bitmap Array (it happens of the main thread!)
|
||||
// Safari < 15 uses the `<img async>` element due to https://bugs.webkit.org/show_bug.cgi?id=182424
|
||||
// Safari > 15 still uses `<img async>` due to their buggy createImageBitmap not handling EXIF rotation
|
||||
async function fileToDrawable(file) {
|
||||
const caps = helperContext().capabilities;
|
||||
|
||||
if ("createImageBitmap" in self && !caps.isApple) {
|
||||
return await createImageBitmap(file);
|
||||
} else {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.decoding = "async";
|
||||
img.src = url;
|
||||
const loaded = new Promise((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(Error("Image loading error"));
|
||||
});
|
||||
|
||||
if (img.decode) {
|
||||
// Nice off-thread way supported in Safari/Chrome.
|
||||
// Safari throws on decode if the source is SVG.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=188347
|
||||
await img.decode().catch(() => null);
|
||||
}
|
||||
|
||||
// Always await loaded, as we may have bailed due to the Safari bug above.
|
||||
await loaded;
|
||||
return img;
|
||||
}
|
||||
return await createImageBitmap(file);
|
||||
}
|
||||
|
||||
function drawableToImageData(drawable) {
|
||||
|
@ -40,17 +10,7 @@ function drawableToImageData(drawable) {
|
|||
sw = width,
|
||||
sh = height;
|
||||
|
||||
const offscreenCanvasSupported = typeof OffscreenCanvas !== "undefined";
|
||||
|
||||
// Make canvas same size as image
|
||||
let canvas;
|
||||
if (offscreenCanvasSupported) {
|
||||
canvas = new OffscreenCanvas(width, height);
|
||||
} else {
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
let canvas = new OffscreenCanvas(width, height);
|
||||
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
@ -60,10 +20,6 @@ function drawableToImageData(drawable) {
|
|||
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
|
||||
if (!offscreenCanvasSupported) {
|
||||
canvas.remove();
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ export default class MediaOptimizationWorkerService extends Service {
|
|||
workerUrl = getAbsoluteURL("/javascripts/media-optimization-worker.js");
|
||||
currentComposerUploadData = null;
|
||||
promiseResolvers = null;
|
||||
workerDoneCount = 0;
|
||||
workerPendingCount = 0;
|
||||
|
||||
async optimizeImage(data, opts = {}) {
|
||||
this.promiseResolvers = this.promiseResolvers || {};
|
||||
|
@ -98,6 +100,7 @@ export default class MediaOptimizationWorkerService extends Service {
|
|||
},
|
||||
[imageData.data.buffer]
|
||||
);
|
||||
this.workerPendingCount++;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -147,7 +150,9 @@ export default class MediaOptimizationWorkerService extends Service {
|
|||
this.workerInstalled = false;
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
this.workerDoneCount = 0;
|
||||
}
|
||||
this.workerPendingCount = 0;
|
||||
}
|
||||
|
||||
registerMessageHandler() {
|
||||
|
@ -163,6 +168,13 @@ export default class MediaOptimizationWorkerService extends Service {
|
|||
|
||||
this.promiseResolvers[e.data.fileId](optimizedFile);
|
||||
|
||||
this.workerDoneCount++;
|
||||
this.workerPendingCount--;
|
||||
if (this.workerDoneCount > 4 && this.workerPendingCount === 0) {
|
||||
this.logIfDebug("Terminating worker to release memory in WASM.");
|
||||
this.stopWorker();
|
||||
}
|
||||
|
||||
break;
|
||||
case "error":
|
||||
this.logIfDebug(
|
||||
|
@ -174,6 +186,7 @@ export default class MediaOptimizationWorkerService extends Service {
|
|||
}
|
||||
|
||||
this.promiseResolvers[e.data.fileId]();
|
||||
this.workerPendingCount--;
|
||||
break;
|
||||
case "installed":
|
||||
this.logIfDebug("Worker installed.");
|
||||
|
|
Loading…
Reference in New Issue