41

We are working on a visualization web application which use d3-force to draw a network on a canvas.

But now we’ve got a problem with browsers on iOS, where the process crashes after few interactions with the interface. To my recollection, this was not a problem with older version (prior to iOS12), but I don’t have any not-updated-device to confirm this.

I think this code summarizes the problem :

const { range } = require('d3-array')

// create a 1MB image
const createImage = () => {
    const size = 512

    const canvas = document.createElement('canvas')
    canvas.height = size
    canvas.width = size

    const ctx = canvas.getContext('2d')
    ctx.strokeRect(0, 0, size, size)
    return canvas
}

const createImages = i => {
    // create i * 1MB images
    let ctxs = range(i).map(() => {
        return createImage()
    })
    console.log(`done for ${ctxs.length} MB`)
    ctxs = null
}

window.cis = createImages

Then on an iPad and in the inspector :

> cis(256)
[Log] done for 256 MB (main-a9168dc888c2e24bbaf3.bundle.js, line 11317)
< undefined
> cis(1)
[Warning] Total canvas memory use exceeds the maximum limit (256 MB). (main-a9168dc888c2e24bbaf3.bundle.js, line 11307)
< TypeError: null is not an object (evaluating 'ctx.strokeRect')

Being, I create 256 x 1MB canvas, everything goes well, but I create one more and the canvas.getContext returns a null pointer. It is then impossible to create another canvas.

The limit seems to be device related as on the iPad its is 256MB and on an iPhone X it is 288MB.

> cis(288)
[Log] done for 288 MB (main-a9168dc888c2e24bbaf3.bundle.js, line 11317)
< undefined
> cis(1)
[Warning] Total canvas memory use exceeds the maximum limit (288 MB). (main-a9168dc888c2e24bbaf3.bundle.js, line 11307)
< TypeError: null is not an object (evaluating 'ctx.strokeRect')

As it is a cache I should be able to delete some elements, but I’m not (as setting ctxs or ctx to null should trigger the GC, but it does not solve the problem).

The only relevant page I found on this problem is a webkit source code page: HTMLCanvasElement.cpp.

I suspect the problem could come from webkit itself, but I’m would like to be sure before posting to webkit issue tracker.

Is there another way to destroy the canvas contexts ?

Thanks in advance for any idea, pointer, ...

UPDATE

I found this Webkit issue which is (probably) a description of this bug: https://bugs.webkit.org/show_bug.cgi?id=195325

To add some informations, I tried other browsers. Safari 12 has the same problem on macOS, even if the limit is higher (1/4 of the computer memory, as stated in webkit sources). I also tried with the latest webkit build (236590) without more luck. But the code works on Firefox 62 and Chrome 69.

I refined the test code, so it can be executed directly from the debugger console. It would be really helpful if someone could test the code on an older safari (like 11).

let counter = 0

// create a 1MB image
const createImage = () => {
    const size = 512

    const canvas = document.createElement('canvas')
    canvas.height = size
    canvas.width = size

    const ctx = canvas.getContext('2d')
    ctx.strokeRect(0, 0, size, size)
    return canvas
}

const createImages = n => {
    // create n * 1MB images
    const ctxs = []

    for( let i=0 ; i<n ; i++ ){
        ctxs.push(createImage())
    }

    console.log(`done for ${ctxs.length} MB`)
}

const process = (frequency,size) => {
    setInterval(()=>{
        createImages(size)
        counter+=size
        console.log(`total ${counter}`)
    },frequency)
}


process(2000,1000)
sandstrom
  • 14,554
  • 7
  • 65
  • 62
Ogier Maitre
  • 728
  • 1
  • 5
  • 12
  • 1
    do you intent to create a canvas element for every rect? – enxaneta Sep 27 '18 at 09:08
  • For such a viz, you might want to look into webgl... Getting so many clear texts + zoom + panning will get hard for 2d context on low end devices anyway. You could try to only save the sankey links on a canvas, and redraw all nodes every frame, kind of a mix of the two worlds, but a canvas per node is indeed too much. Ps: I suspect this is a HW limitation more than a software one. – Kaiido Sep 27 '18 at 09:59
  • Thanks for these idea. It's a bit of a nightmare to draw those things in webgl (e.g. fonts). The project was using svg at first, and was modified to use canvas. So to me it was more efficient to implement this "cache" mechanism instead of WebGL. I'm not sure about the HW limitation either, because I think it was working before (on iOS10) and more importantly canvas are not loaded in DOM, so they are (to my understanding) no more than data array. – Ogier Maitre Sep 27 '18 at 11:02
  • @enxaneta this code is pretty much just to show my main problem. In the app, each canvas contains a node. – Ogier Maitre Sep 27 '18 at 11:04
  • @OgierMaitre Have you ever find any solution for this issue? – Sachin Kumaram Jul 03 '19 at 07:32

8 Answers8

18

Someone posted an answer, that showed a workaround for this. The idea is to set height and width to 0 before deleting the canvases. It is not really a proper solution, but it will work in my cache system.

I add a small example that creates canvases until an exception is thrown, then empties the cache and continues.

Thank to the now anonymous person who posted this answer.

let counter = 0

// create a 1MB image
const createImage = () => {
    const size = 512

    const canvas = document.createElement('canvas')
    canvas.height = size
    canvas.width = size

    const ctx = canvas.getContext('2d')
    ctx.strokeRect(0, 0, size, size)
    return canvas
}

const createImages = nbImage => {
    // create i * 1MB images
    const canvases = []

    for (let i = 0; i < nbImage; i++) {
        canvases.push(createImage())
    }

    console.log(`done for ${canvases.length} MB`)
    return canvases
}

const deleteCanvases = canvases => {
    canvases.forEach((canvas, i, a) => {
        canvas.height = 0
        canvas.width = 0
    })
}

let canvases = []
const process = (frequency, size) => {
    setInterval(() => {
        try {
            canvases.push(...createImages(size))
            counter += size
            console.log(`total ${counter}`)
        }
        catch (e) {
            deleteCanvases(canvases)
            canvases = []
        }
    }, frequency)
}


process(2000, 1000)
Ogier Maitre
  • 728
  • 1
  • 5
  • 12
  • 3
    I use Google Maps in a web environment. Google Maps draws the map in a canvas. Everything worked great until upgrade to IOS12. The "Total Canvas memory use..." error and "...null is not an object reading a.scale" errors showed up, and everything breaks after a few pan or zooms. This was with no code changes on my part. Everything still works on older devices running IOS 9 and 10. Something changed in Apple Safari Land on IOS. MacBook Pro with Mojave (19.14) has no problems. Not an answer, but, hopefully this clarifies your problem by seeing similar issues from a different use case. – eepete Oct 02 '18 at 15:32
  • I just install Mojave, so I will try tomorrow. But I’m pretty sure the problem still exists. You just have to consume a larger amount of memory. I’ll let you know. – Ogier Maitre Oct 03 '18 at 16:35
  • Thanks for the test on older systems. – Ogier Maitre Oct 03 '18 at 16:35
  • So, I tried on Mojave Safari and the problem is still the same, but as I said, on my computer, the problem happens around 4GB of canvas, which is harder/longer to attain. – Ogier Maitre Oct 04 '18 at 11:37
  • 1
    We had this problem as well. We're using canvasses in a serie of drawing questions that can be scrolled through. It's a React app using https://github.com/michaeldzjap/react-signature-pad-wrapper. Adding `if (this._canvas) { this._canvas.height = 0; this._canvas.width = 0; }` in `componentWillUnmount` seems to have fixed the problem. We can't get the error message anymore as much as we try to abuse it scrolling and reloading. – Christiaan Westerbeek Sep 06 '19 at 16:59
  • This problem still persists. The walkaround is reducing the canvas size to 0 every time after you done using it. If you have to cache the canvas, convert it to image and cache the image instead – Duannx Aug 02 '20 at 16:28
  • Still occurs in 2022 with the latest Safari. Close application, and reopen. – Chewie The Chorkie Jun 29 '22 at 22:28
12

Another data-point: I've found that the Safari Web Inspector (12.1 - 14607.1.40.1.4) holds on to every Canvas object created while it is open, even if they would otherwise be garbage-collected. Close the web-inspector and re-open it and most of the old canvases go away.

This doesn't solve the original problem - exceeding canvas memory when NOT running web-inspector, but without knowing this little tid-bit, I wasted a bunch of time going down the wrong path thinking I wasn't releasing any of my temporary canvases.

6

I spent the weekend making a simple web page that can quickly show the problem. I have submitted bug reports to Google and Apple. The page brings up a map. You can pan and zoom all you want, and the Safari inspector (running web on iPad, using MacBook Pro to see the canvases) see no canvas.

You can then tap a button and draw one polyline. When you do that, you see 41 canvases. Pan or zoom, and you'll see more. Each canvas is 1MB, so after you have 256 of the orphaned canvases, errors start as the canvas memory on the iPad is full.

Reload the page, tap a button to place one polygon, and the same thing happens.

Equally interesting is I added buttons to style the map for day and night. You can go back and forth when it's just a map (or a map with only markers, there is a button to display some markers on the map). No orphaned canvases. But draw a line, and then when you change the styling, you see more orphaned canvases.

Looking at Safari on the MacBook in the Active Monitor, the size just keeps going as you pan and zoom around the map after drawing a poly*

I hope Apple and Google can figure it out and not claim that it is the other company's problem. All this changed with IOS12 running web pages that have been stable for years, and that still work on IOS 9 and 10 iPads I keep for testing to be sure that older devices can show the current web pages. Hope this test/experiment helps.

eepete
  • 156
  • 6
  • can you share a link to the issues you opened with apple and google? – James Gilchrist Oct 16 '18 at 14:22
  • 1
    Not sure how to share that. Nothing from Apple yet. Google is looking at it, but they suspect it's an Apple problem. It all comes down to as soon as you create a polygon or polyline, Safari shows lots of new canvases every time you pan or zoom. Eventually, you run of of canvas memory (IOS) or have a hug memory footprint. This does not happen on older IOS or my older MAC Pro that can't be updated anymore (thanks Apple....). It's that simple to describe the problem. – eepete Oct 16 '18 at 18:33
  • can you share the sample web page? – Brandon McAlees Nov 05 '18 at 15:00
  • 2
    Google has correctly decided this is an Apple problem- there was no change to their javascript library in the timeframe of the IOS12 update. It's up to Apple now. I sent them a sys diagnose dump, and await acknowledgement of the problem from them. The number at the top of the bug report is 45077639 – eepete Nov 08 '18 at 16:40
  • 3
    The new IOS 12.1 does _not_ fix this bug in Safari. – eepete Nov 13 '18 at 14:34
  • You can't see Apple issues as this link say -> https://stackoverflow.com/questions/144873/can-i-browse-other-peoples-apple-bug-reports And as it say you are encouraged to send new bug report if you encounter this bug to increase the severity of the bug in their bug report board. – Etienne Prothon Jan 07 '19 at 13:29
  • 2
    Not fixed as of 12.3.1 – eepete May 30 '19 at 18:56
  • Still not fixed as of 25 Aug 2020. They are aware of the issue. The URL for the issue tracker is: https://issuetracker.google.com/issues/142335161 – eepete Aug 26 '20 at 13:39
5

Probably this recent change in WebKit should be causing these issues https://github.com/WebKit/webkit/commit/5d5b478917c685e50d1032ccf761ca53fc8f1b74#diff-b411cd4839e4bbc17b00570536abfa8f

Rathaiah
  • 59
  • 1
  • 4
    Please add some explanation to your answer and about the change that comes with that commit, not just the link. – f-CJ Oct 11 '18 at 21:46
4

I had this problem for a long time, but it seems I was able to fix it today. I used a canvas and drawn on it several times without having a problem. However, sometimes after some resizing I got the exception "Total canvas memory usage exceeds the maximum limit", and my canvas seemed to have disappeared...

My solution was to reduce the size of the canvas to 0 and then delete the entire canvas. Afterwards initialize a new canvas after the resize happened.

DoResize();

if (typeof canvas === "object" && canvas !== null) {
    canvas.width = 0;
    canvas.height = 0;

    canvas.remove();
    delete canvas;
    canvas = null;
}

canvas = document.createElement("canvas");              
container.appendChild(canvas);

// Just in case, wait for the Browser
window.requestAnimationFrame(() => {
    let context = canvas.getContext("2d");
    context.moveTo(10, 10);
    context.lineTo(30, 30);
    context.stroke();
});

The requestAnimationFrame was not necessarily needed but I just wanted to wait for the Device to update the canvas. I tested that with an iPhone XS Max.

MEN
  • 547
  • 6
  • 7
3

I can confirm this problem. No change to existing code that worked for years. However, in my case, the canvas is drawn only once when the page is loaded. Users can then browse between different canvases and the browser does a page reload.

My debugging attempts so far show that Safari 12 apparently leaks memory between page reloads. Profiling memory consumption via Web Inspector shows that the memory keeps growing for each page reload. Chrome and Firefox on the other hand seem to keep memory consumption at the same level.

From a user perspective, it helps to simply wait 20-30 seconds and do a page reload. Safari clears memory in the meantime.

Edit: Here's a minimal proof of concept that shows how Safari 12 leaks memory between page loads.

01.html

<a href="02.html">02</a>
<canvas id="test" width="10000" height="1000"></canvas>
<script>
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#0000ff";
ctx.fillRect(0,0,10000,1000);
</script>

02.html

<a href="01.html">01</a>
<canvas id="test" width="10000" height="1000"></canvas>
<script>
var canvas = document.getElementById("test");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "#00FF00";
ctx.fillRect(0,0,10000,1000);
</script>

Steps to reproduce:

  • Upload both files to a webserver
  • Click the link on top repeatedly to switch between pages
  • Watch Web Inspector memory consumption go up for every page load

I submitted a bug report to Apple. Will see how this works out.

enter image description here

Edit: I updated the dimensions of the Canvas to 10000x1000 as a better proof of concept. If you now upload both files to a server and run it on your iOS device, if you rapidly switch between pages, the Canvas won't be drawn after several page reloads. If you then wait 30-60 seconds, some cache seems to be getting cleared and a reload will again show the Canvas.

ntaso
  • 614
  • 6
  • 12
  • This is similar to what I'm seeing in IOS12. Code that has worked for years if failing. The difference is I don't have to reload a page, I just have to create canvases and then release them on the same page. For me, this is done when using the Google Maps javascript file when you do anything with *polys (poly lines or polygons). An operation shows 40-50 "orphaned" canvases with the Safari debugger looking at the web page on the IOS device – eepete Nov 06 '18 at 16:43
  • This is a fantastic minimal example. Have you sent this example in to Apple so they can see how easy it is to reproduce the problem? – setholopolus Dec 13 '18 at 19:55
  • 1
    @setholopolus Yes and they closed it, because they said the overhead comes from the developer tools. I then updated the demo and wrote a comment to the closed issue. It says somewhere that they'd read and consider comments on closed issues. But nothing so far. Maybe I have to file a new issue. Of course, this has nothing to do with the developer tools (you can close them and still see the canvas not being drawn). The memory leak does exist! – ntaso Dec 14 '18 at 12:30
1

Just wanted to say we have a web application using Three.js crashing on iPad Pro (1st gen) on iOS 12. Upgrading to iOS 13 Public Beta 7 fixed the issue. The application isn't crashing anymore.

Shay Cojo
  • 73
  • 7
0

I've submitted a new bug report to Apple, no reply yet. I added the ability to execute the code shown below after I drew a line using Polylines in google maps:

function makeItSo(){
  var foo = document.getElementsByTagName("canvas");
  console.log(foo);
  for(var i=0;i < foo.length;i++){
    foo[i].width = 32;
    foo[i].height = 32;
  }
}

Looking at the console output, only 4 canvas elements were found. But looking at the "canvas" panel in the Safari debugger, there were 33 canvases displayed (amount depends on the size of the web page you have open).
After the above code runs, the canvases display shows the 4 canvasses that were found at a smaller size, as one might expect. All the other "orphaned" canvases are still displayed in the debugger.
I suspect this confirms the "memory leak" theory- canvases that exists but are not in the document. When the amount of canvas memory you have is exceeded, nothing more can be rendered using canvases.
Again, all this worked until IOS12. My older iPad running IOS 10 still works.

Zac
  • 730
  • 1
  • 12
  • 19
eepete
  • 156
  • 6
  • Not fixed as of the latest 12.3.1 release. – eepete May 30 '19 at 18:55
  • 1
    While it could be a "memory leak", it could also just be that the canvas objects are created in code and never added to the dom. – Peter Aug 26 '20 at 05:45
  • Peter: agreed. Here is the link to the issue. It's still disturbing that this has been around for 2 years with no fix from the Google Maps Team. Apple also claims it is not their bug. When tech titans clash at the expense of users... It may well be that the canvas objects are created, but an error is tossed and that keeps them from being addd to the DOM. Look at the URL, they are trying to get the errors so it propagate back into UserLand. https://issuetracker.google.com/issues/142335161 – eepete Aug 26 '20 at 13:45