Reputation: 267
I need some help with finding a memory leak in a small, Browser / WebWorker JavaScript. I tracked it down into this little piece of code:
/**
* Resizes an Image
*
* @function scaleImage
* @param {object} oImageBlob blob of image
* @param {int} iHeight New height of Image
* @return {ImageBitmap} ImageBitmap Object
*/
async function scaleImage(oImageBlob, iHeight) {
var img = await self.createImageBitmap(oImageBlob);
var iWidth = Math.round( ( img.width / img.height ) * iHeight);
var canvas = new OffscreenCanvas(iWidth,iHeight);
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return(canvas.transferToImageBitmap());
}
It's called from:
[inside a web worker: Some looping that calls this about 1200 times while parsind files from a s3 bucket ...]
var oImageBlob = await new Response(oS3Object.Body, {}).blob();
var oThumbnail = await scaleImage(oImageBlob, 100);
await IDBputData(oInput.sFileKey, oImageBlob, oInput.sStore, oThumbnail)
[... end of the loop]
The other interior function is
/**
* Put information to IndexedDB
*
* @function IDBputData
* @param {string} sKey key of information
* @param {string} sValue information to upload
* @param {string} sStore name of object store
* @param {object} oThumbnail optrional, default: null, thumbnail image
* @return {object} - SUCCESS: array, IndexedDB Identifyer "key"
* - FAIL: Error Message
*/
async function IDBputData(sKey, sValue, sStore, oThumbnail=null) {
var oGeneratedKeys = {};
if(sStore=="panelStore"){
oGeneratedKeys = await getKeyfromSKey(sKey);
}
return new Promise((resolve, reject) => {
const tx = oConn.transaction(sStore, 'readwrite');
const store = tx.objectStore(sStore);
var request = {}
request = store.put({panelkey: oGeneratedKeys.panelkey, layerkey: oGeneratedKeys.layerkey, countrycode: oGeneratedKeys.countrycode, value: sValue, LastModified: new Date(), oThumbnail: oThumbnail});
request.onsuccess = () => (oThumbnail.close(),resolve(request.result));
request.onerror = () => (oThumbnail.close(),reject(request.error));
});
}
When I run it this way in Chrome, it will consume every bit of RAM I've got free (around 8 GB) and then crash. (Laptop with shared RAM for CPU/GPU).
When I change
var oThumbnail = await scaleImage(oImageBlob, 100);
to
var oThumbnail = null;
RAM consumption of Chrome stays rather fixed around 800 MB, so there must be something with the topmost function "scaleImage".
I tried tweaking it, but with no success.
/**
* Resizes an Image
*
* @function scaleImage
* @param {object} oImageBlob blob of image
* @param {int} iHeight New height of Image
* @return {ImageBitmap} ImageBitmap Object
*/
async function scaleImage(oImageBlob, iHeight) {
var img = await self.createImageBitmap(oImageBlob);
var iWidth = Math.round( ( img.width / img.height ) * iHeight);
var canvas = new OffscreenCanvas(iWidth,iHeight);
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
var oImageBitmap = canvas.transferToImageBitmap();
ctx = null;
canvas = null;
iWidth = null;
img = null;
return(oImageBitmap);
}
Any help is very much appreciated.
Upvotes: 1
Views: 1380
Reputation: 136856
For the ImageBitmap to release its bitmap data the most efficient way, you have to call its .close()
method once you're done with it.
But actually, you don't need this scaleImage
function. createImageBitmap()
has a resizeHeight
option, and if you use it without the resizeWidth
one, you'll resize your image by keeping the aspect-ratio exacty like you are doing in your function, except that it won't need to assign the bitmap twice.
Once you have this resized ImageBitmap, you can transfer it to a BitmapRenderingContext (which will internally close()
the original ImageBitmap) and call the transferToBlob()
from that renderer.
This should be lighter for your computer.
async function worker_script() {
const blob = await fetch( "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png" ).then( (resp) => resp.blob() );
// resize from createImageBitmap directly
const img = await createImageBitmap( blob , { resizeHeight: 100 } );
const canvas = new OffscreenCanvas( img.width, img.height );
canvas.getContext( "bitmaprenderer" )
.transferFromImageBitmap( img ); // this internally closes the ImageBitmap
const resized_blob = await canvas.convertToBlob();
// putInIDB( resized_blob );
// for the demo we pass that Blob to main, but one shouldn't have to do that
// show the canvas instead ;)
postMessage( { blob: resized_blob, width: img.width } );
// to be safe, we can even resize our canvas to 0x0 to free its bitmap entirely
canvas.width = canvas.height = 0;
}
// back to main
const worker = new Worker( getWorkerURL() );
worker.onmessage = ({ data: { blob, width } }) => {
const img = new Image();
img.src = URL.createObjectURL( blob );
img.onload = (evt) => URL.revokeObjectURL( img.src );
document.body.append( img );
console.log( "ImageBitmap got detached?", width === 0 );
};
function getWorkerURL() {
const content = "!" + worker_script.toString() + "();";
const blob = new Blob( [ content ], { type: "text/javascript" } );
return URL.createObjectURL( blob );
}
Upvotes: 3