Total canvas memory use exceeds the maximum limit (Safari 12)

18,361

Solution 1

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)

Solution 2

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.

Solution 3

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.

Solution 4

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

Solution 5

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.

Share:
18,361
Ogier Maitre
Author by

Ogier Maitre

Updated on June 23, 2022

Comments

  • Ogier Maitre
    Ogier Maitre almost 2 years

    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)