28

I have a webpage that rapidly streams JSON from the server and displays bits of it, about 10 times/second. One part is a base64-encoded PNG image. I've found a few different ways to display the image, but all of them cause unbounded memory usage. It rises from 50mb to 2gb within minutes. Happens with Chrome, Safari, and Firefox. Haven't tried IE.

I discovered the memory usage first by looking at Activity Monitor.app -- the Google Chrome Renderer process continuously eats memory. Then, I looked at Chrome's Resource inspector (View > Developer > Developer Tools, Resources), and I saw that it was caching the images. Every time I changed the img src, or created a new Image() and set its src, Chrome cached it. I can only imagine the other browsers are doing the same.

Is there any way to control this caching? Can I turn it off, or do something sneaky so it never happens?

Edit: I'd like to be able to use the technique in Safari/Mobile Safari. Also, I'm open to other methods of rapidly refreshing an image if anyone has any ideas.

Here are the methods I've tried. Each one resides in a function that gets called on AJAX completion.

Method 1 - Directly set the src attribute on an img tag

Fast. Displays nicely. Leaks like crazy.

$('#placeholder_img').attr('src', 'data:image/png;base64,' + imgString);

Method 2 - Replace img with a canvas, and use drawImage

Displays fine, but still leaks.

var canvas = document.getElementById("placeholder_canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
    ctx.drawImage(img, 0, 0); 
}   
img.src = "data:image/png;base64," + imgString;

Method 3 - Convert to binary and replace canvas contents

I'm doing something wrong here -- the images display small and look like random noise. This method uses a controlled amount of memory (grows to 100mb and stops), but it is slow, especially in Safari (~50% CPU usage there, 17% in Chrome). The idea came from this similar SO question: Data URI leak in Safari (was: Memory Leak with HTML5 canvas)

var img = atob(imgString);
var binimg = [];
for(var i = 0; i < img.length; i++) {
    binimg.push(img.charCodeAt(i));
}
var bytearray = new Uint8Array(binimg);

// Grab the existing image from canvas
var ctx = document.getElementById("placeholder_canvas").getContext("2d");
var width = ctx.canvas.width, 
    height = ctx.canvas.height;
var imgdata = ctx.getImageData(0, 0, width, height);

// Overwrite it with new data
for(var i = 8, len = imgdata.data.length; i < len; i++) {
    imgdata.data[i-8] = bytearray[i];
}

// Write it back
ctx.putImageData(imgdata, 0, 0);
Community
  • 1
  • 1
Dave Ceddia
  • 1,480
  • 2
  • 17
  • 24
  • Do you have to use data uris or can you use blob uris? Blob uris are explicitly created and deallocated; you should only be able to leak memory using them if you screw up. Take a look at the compatibility matrix here: http://caniuse.com/#search=blob%20url – ellisbben Mar 28 '12 at 18:48
  • 1
    Method 3 can't work; a canvas' imageData is an array of RGBA values; to put it lightly, PNGs are not saved as a raw array of RGBA values. – ellisbben Mar 28 '12 at 18:58
  • 1
    Ah thanks for that info, that explains the random noise ;) – Dave Ceddia Mar 28 '12 at 20:00
  • method 3 could be modified to work, but to get a pixel array that you can use (buffer) it's possible that it will still leak. if you get the rgba data from an image you can write it to a canvas and then display it – Mark Essel Jun 22 '12 at 08:44
  • here's an example of writing to image data, it's not super clear though (a few things going on). let me know if you are still interested and I can find a purer case. http://victusfate.github.com/html5_layered_ripple – Mark Essel Jun 22 '12 at 08:47
  • did you get this working ? – gsagrawal Mar 07 '16 at 10:45
  • @gsagrawal - Sorry to say I didn't. This was an experimental prototype UI for a data collection system. We ended up switching gears and writing a native client in Qt, and I moved on to a different job a few years ago. – Dave Ceddia Mar 07 '16 at 14:51
  • This memory leak has been reported as a WebKit bug back in 2009. As of today it is still not fixed. https://bugs.webkit.org/show_bug.cgi?id=31253 – Pierre F Jul 16 '18 at 15:18
  • This is still a problem. See https://stackoverflow.com/questions/71151958/memory-leak-in-ios-when-loading-images-from-external-url-and-using-createobjectu – Avner Moshkovitz Feb 17 '22 at 03:21

8 Answers8

5

I know it's been years since this issue was posted, but the problem still exists in recent versions of Safari Browser. So I have a definitive solution that works in all browsers, and I think this could save jobs or lives!.

Copy the following code somewhere in your html page:

// Methods to address the memory leaks problems in Safari
var BASE64_MARKER = ';base64,';
var temporaryImage;
var objectURL = window.URL || window.webkitURL;

function convertDataURIToBlob(dataURI) {
    // Validate input data
    if(!dataURI) return;

    // Convert image (in base64) to binary data
    var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
    var base64 = dataURI.substring(base64Index);
    var raw = window.atob(base64);
    var rawLength = raw.length;
    var array = new Uint8Array(new ArrayBuffer(rawLength));

    for(i = 0; i < rawLength; i++) {
        array[i] = raw.charCodeAt(i);
    }

    // Create and return a new blob object using binary data
    return new Blob([array], {type: "image/jpeg"});
}

Then when you receive a new frame/image base64Image in base64 format (e.g. data:image/jpeg;base64, LzlqLzRBQ...) and you want to update a html <img /> object imageElement, then use this code:

// Destroy old image
if(temporaryImage) objectURL.revokeObjectURL(temporaryImage);

// Create a new image from binary data
var imageDataBlob = convertDataURIToBlob(base64Image);

// Create a new object URL object
temporaryImage = objectURL.createObjectURL(imageDataBlob);

// Set the new image
imageElement.src = temporaryImage;

Repeat this last code as much as needed and no memory leaks will appear. This solution doesn't require the use of the canvas element, but you can adapt the code to make it work.

Pierre F
  • 1,332
  • 15
  • 33
Panchosoft
  • 51
  • 1
  • 2
  • The use of revokObjectURL is not working on iOS. See description of the problem and life example in https://stackoverflow.com/questions/71122643/webapp-fails-in-ios-when-using-revokeobjecturl – Avner Moshkovitz Feb 16 '22 at 06:41
  • A simplified example that demonstrates the problem on iOS is shown in https://stackoverflow.com/questions/71151958/memory-leak-in-ios-when-loading-images-from-external-url-and-using-createobjectu – Avner Moshkovitz Feb 17 '22 at 03:22
3

Try setting image.src = "" after drawing.

var canvas = document.getElementById("placeholder_canvas");
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = function() {
    ctx.drawImage(img, 0, 0); 
    //after drawing set src empty
    img.src = "";
}   
img.src = "data:image/png;base64," + imgString;

This might be helped

Jakke
  • 31
  • 2
3

I don't think there are any guarantees given about the memory usage of data URLs. If you can figure out a way to get them to behave in one browser, it guarantees little if not nothing about other browsers or versions.

If you put your image data into a blob and then create a blob URL, you can then deallocate that data.

Here's an example which turns a data URI into a blob URL; you may need to change / drop the webkit- & WebKit- prefixes on browsers other than Chrome and possibly future versions of Chrome.

var parts = dataURL.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+/]+)/);

//assume base64 encoding
var binStr = atob(parts[3]);

//might be able to replace the following lines with just
// var view = new Uint8Array(binStr);
//haven't tested.

//convert to binary in ArrayBuffer
var buf = new ArrayBuffer(binStr.length);
var view = new Uint8Array(buf);
for(var i = 0; i < view.length; i++)
  view[i] = binStr.charCodeAt(i);
//end of the possibly unnecessary lines

var builder = new WebKitBlobBuilder();
builder.append(buf);

//create blob with mime type, create URL for it
var URL = webkitURL.createObjectURL(builder.getBlob(parts[1]))
return URL;

Deallocating is as easy as :

webkitURL.revokeObjectURL(URL);

And you can use your blob URL as your img's src.

Unfortunately, blob URLs do not appear to be supported in IE prior to v10.

API reference:

http://www.w3.org/TR/FileAPI/#dfn-createObjectURL

http://www.w3.org/TR/FileAPI/#dfn-revokeObjectURL

Compatibility reference:

http://caniuse.com/#search=blob%20url

Alon Amir
  • 4,913
  • 9
  • 47
  • 86
ellisbben
  • 6,352
  • 26
  • 43
  • Unfortunately it also doesn't seem to work in Safari or Mobile Safari :( I've got it working in Chrome, though. I left it running overnight and it used up a bunch of memory (~700mb) but I might be doing something wrong in deallocating the blobs. Something that works in Safari would be better. – Dave Ceddia Mar 29 '12 at 13:04
  • 700 mb might just be the steady state that it reaches with garbage collection. Beats 2GB after a few minutes though, doesn't it? :) Without blob urls, you have to find some indirect way of getting the browser to have the memory behavior you want it to. See my other answer. – ellisbben Mar 29 '12 at 14:54
  • Can you guys comment on the way you are actually setting the img's src? I tried `$("#imgId").attr('src', URL);` but no good. – Boro Apr 27 '12 at 14:39
  • Don't know what framework you are using, but without any framework you should be able to do `document.getElementById('imgId').src = URL;` or `document.getElementById('imgId').setAttribute('src', URL);` – ellisbben Apr 27 '12 at 14:41
  • In Chrome v83, even using blob-urls (with revoking of them after usage) still leads to the ever-increasing memory usage/leak. (my app starts at ~300mb, but expands to >4.5gb if left to keep processing frames!) – Venryx Jun 25 '20 at 20:48
  • 1
    I still see memory leaks with iOS WKWebView based browsers, and native app. I posted the problem and an example in https://stackoverflow.com/questions/71122643/webapp-fails-in-ios-when-using-revokeobjecturl – Avner Moshkovitz Feb 16 '22 at 06:39
2

I had a very similar issue.

Setting img.src to dataUrl Leaks Memory

Long story short, I simply worked around the Image element. I use a javascript decoder to decode and display the image data onto a canvas. Unless the user tries to download the image, they'll never know the difference either. The other downside is that you're going to be limited to modern browsers. The up side is that this method doesn't leak like a sieve :)

Community
  • 1
  • 1
Paul Milham
  • 526
  • 5
  • 13
2

patching up ellisbben's answer, since BlobBuilder is obsoleted and https://developer.mozilla.org/en-US/Add-ons/Code_snippets/StringView provides what appears to be a nice quick conversion from base64 to UInt8Array:

in html:

<script src='js/stringview.js'></script>

in js:

window.URL =    window.URL ||
                window.webkitURL;
function blobify_dataurl(dataURL){
    var parts = dataURL.match(/data:([^;]*)(;base64)?,([0-9A-Za-z+/]+)/);

    //assume base64 encoding
    var binStr = atob(parts[3]);

    //convert to binary in StringView
    var view = StringView.base64ToBytes(parts[3]);

    var blob = new Blob([view], {type: parts[1]}); // pass a useful mime type here

    //create blob with mime type, create URL for it
    var outURL = URL.createObjectURL(blob);
    return outURL;
}

I still don't see it actually updating the image in Safari mobile, but chrome can receive dataurls rapid-fire over websocket and keep up with them far better than having to manually iterate over the string. And if you know you'll always have the same type of dataurl, you could even swap the regex out for a substring (likely quicker...?)

Running some quick memory profiles, it looks like Chrome is even able to keep up with deallocations (if you remember to do them...):

URL.revokeObjectURL(outURL);
1

I have used different methods to solve this problem, none of them works. It seems that memory leaks when img.src = base64string and those memory can never get released. Here is my solution.

fs.writeFile('img0.jpg', img_data, function (err) {
    // console.log("save img!" );
});
document.getElementById("my-img").src =  'img0.jpg?'+img_step;
img_step+=1;

Note that you should convert base64 to jpeg buffer.

My Electron app updating img every 50ms, and memory doesn't leak. Forget about disk usage. Chrome's memory management piss me off.

Keven Sun
  • 41
  • 2
0
var inc = 1;
                        var Bulk = 540;
                        var tot = 540;
                        var audtot = 35.90;
                        var canvas = document.getElementById("myCanvas");
                        //var imggg = document.getElementById("myimg");
                        canvas.width = 550;
                        canvas.height = 400;
                        var context = canvas.getContext("2d");
                        var variation = 0.2;
                        var interval = 65;
                        function JLoop() {
                            if (inc < tot) {

                                if (vid.currentTime < ((audtot * inc) / tot) - variation || (vid.currentTime > ((audtot * inc) / tot) + variation)) {
                                    contflag = 1;
                                    vid.currentTime = ((audtot * inc) / tot);
                                }
                                // Draw the animation
                                try {
                                    context.clearRect(0, 0, canvas.width, canvas.height);
                                    if (arr[inc - 1] != undefined) {
                                      context.drawImage(arr[inc - 1], 0, 0, canvas.width, canvas.height);

arr[inc - 1].src = "";

                                    //document.getElementById("myimg" + inc).style.display = "block";;
                                    //    document.getElementById("myimg" + (inc-1)).style.display = "none";
                                    //imggg.src = arr[inc - 1].src;
                                    }
                                    $("#audiofile").val(inc);

                                   // clearInterval(ref);
                                } catch (e) {
                                }

                                inc++;
                                // interval = 60;
                                //setTimeout(JLoop, interval);
                            }
                            else {

                            }
                        }
                        var ref = setInterval(JLoop, interval);
                    });

Worked for me on memory leak thanks dude.

Jay
  • 274
  • 1
  • 4
  • 12
0

Unless Safari or Mobile Safari don't leak data urls, server-side might be the only way to do this on all browsers.

Probably most straightforward would be to make a URL for your image stream, GETting it gives a 302 or 303 response redirecting to a single-use URL that will give the desired image. You will probably have to destroy and re-create the image tags to force a reload of the URL.

You will also be at the mercy of the browser regarding its img caching behavior. And the mercy of my understanding (or lack of understanding) of the HTTP spec. Still, unless server-side operation doesn't fit your requirements, try this first. It adds to the complexity of the server, but this approach uses the browser much more naturally.

But what about using the browser un-naturally? Depending on how browsers implement iframes and handle their associated content, you might be able to get data urls working without leaking the memory. This is kinda Frankenstein shit and is exactly the sort of nonsense that no one should have to do. Upside: it could work. Downside: there are a bazillion ways to try it and uneven, undocumented behavior is exactly what I'd expect.

One idea: embed an iframe containing a page; this page and the page that it is embedded in use cross document messaging (note the GREEN in the compatibility matrix!); embeddee gets the PNG string and passes it along to the embedded page, which then makes an appropriate img tag. When the embeddee needs to display a new message, it destroys the embedded iframe (hopefully releasing the memory of the data url) then creates a new one and passes it the new PNG string.

If you want to be marginally more clever, you could actually embed the source for the embedded frame in the embeddee page as a data url; however, this might leak that data url, which I guess would be poetic justice for trying such a reacharound.

"Something that works in Safari would be better." Browser technology keeps on moving forward, unevenly. When they don't hand the functionality to you on a plate, you gotta get devious.

ellisbben
  • 6,352
  • 26
  • 43
  • I implemented the `iframe` with cross-document messaging and it did not leak. Consumption was higher than I'd like, but stable (hovered around 200mb). But, deleting/recreating the iframe causes a flicker and slight pause each time, and it looks pretty awful :( I tried a few tricks like populating the iframe in the background w/ a lower z-index, but they didn't seem to have any effect. – Dave Ceddia Apr 02 '12 at 12:58
  • By "populating in the background", do you mean you created a new, non-hidden `iframe` beneath (using `z-index`) the already existing `iframe`, created the `img` inside the new `iframe`, and deleted the previous `iframe` on the success of the `img` load? In theory, the asynchronous delete (via `img.onload` callback) should prevent the old `iframe` from being destroyed before the new one is ready to show. – ellisbben Apr 04 '12 at 04:12