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

Ogier Maitre picture Ogier Maitre · Sep 27, 2018 · Viewed 11.4k times · Source

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)

Answer

Ogier Maitre picture Ogier Maitre · Oct 1, 2018

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)