画布总内存使用量超过了最大限制(Safari 12)

时间:2018-09-27 08:26:54

标签: javascript canvas safari webkit mobile-safari

我们正在研究visualization web application,它使用d3力在画布上绘制网络。 由于每个节点都包含大量信息,并且为每帧重绘所有内容都需要占用大量CPU,因此我们实现了一种缓存,其中每个节点在其画布上绘制(未链接到DOM),每个缩放级别一次。然后,将这些画布绘制在节点位置的主画布(链接到DOM)上。

我们对速度提高感到满意,即使现​​在的结果是占用大量内存(尤其是在hidpi显示器(视网膜)上,像素密度可以是2或3)。

但是现在我们在iOS浏览器上遇到了问题,该过程在与界面进行少量交互后便崩溃了。 我记得,这不是旧版本(iOS12之前的版本)的问题,但我没有任何未更新的设备来确认这一点。

我认为这段代码总结了问题:

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

然后在iPad上和检查器中:

> 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')

是的,我创建了256 x 1MB的画布,一切顺利,但是我又创建了一个画布,canvas.getContext返回一个空指针。 这样就不可能创建另一个画布。

该限制似乎与设备有关,因为iPad上为256MB,iPhone X上为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')

由于它是缓存,因此我应该能够删除某些元素,但我不能删除(因为将ctxs或ctx设置为null会触发GC,但不能解决问题)。

我发现的唯一与此相关的页面是webkit源代码页面:HTMLCanvasElement.cpp

我怀疑问题可能来自webkit本身,但是我想确定一下,然后再发布到webkit问题跟踪器中。

还有另一种销毁画布上下文的方法吗?

在此先感谢您的想法,指导,...

编辑:

要添加一些信息,我尝试了其他浏览器。 Safari 12在macOS上也存在相同的问题,即使限制更高(如Webkit来源所述,为计算机内存的1/4)。我也尝试了最新的Webkit版本(236590),但没有更多的运气。 但是代码可以在Firefox 62和Chrome 69上运行。

我优化了测试代码,因此可以直接在调试器控制台中执行它。如果有人可以在较旧的Safari(例如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)

8 个答案:

答案 0 :(得分:6)

我度过了一个周末,制作了一个可以快速显示问题的简单网页。我已经向Google和Apple提交了错误报告。该页面将显示一个地图。您可以平移和缩放所有所需的内容,而Safari检查器(在iPad上运行网络,使用MacBook Pro查看画布)看不到画布。

然后您可以点击一个按钮并绘制一条折线。当您这样做时,您会看到41幅画布。平移或缩放,您将看到更多。每个画布为1MB,因此在您拥有256个孤立的画布之后,由于iPad上的画布内存已满,错误开始出现。

重新加载页面,点击一个按钮以放置一个多边形,然后发生同样的事情。

同样有趣的是,我添加了按钮来为白天和黑夜设置地图样式。您可以仅在地图上来回移动(或仅带有标记的地图,有一个按钮可以在地图上显示一些标记)。没有孤立的画布。但是画一条线,然后在更改样式时,会看到更多的孤立画布。

在Active Monitor的MacBook上查看Safari的大小,在绘制多边形后平移和缩放地图时,大小会保持不变*

我希望苹果和谷歌能解决这个问题,而不是声称这是另一家公司的问题。 IOS12运行的网页已经稳定多年,并且仍然可以在IOS 9和10 iPad上运行,因此,这一切都发生了变化,我一直进行测试以确保较旧的设备可以显示当前网页。希望这项测试/实验对您有所帮助。

答案 1 :(得分:4)

答案 2 :(得分:3)

有人发布了答案,显示了解决方法。想法是在删除画布之前将高度和宽度设置为0。这不是一个真正合适的解决方案,但它可以在我的缓存系统中使用。

我添加一个小示例,该示例创建画布直到引发异常,然后清空缓存并继续。

感谢现在匿名发布此答案的人。

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)

答案 3 :(得分:1)

我可以确认这个问题。可以更改已有多年使用的现有代码。但是,就我而言,加载页面时画布仅绘制一次。然后,用户可以在不同的画布之间浏览,浏览器会重新加载页面。

到目前为止,我的调试尝试表明Safari 12 显然在两次页面重新加载之间泄漏了内存。通过Web Inspector分析内存消耗情况表明,每次重新加载页面时,内存都在不断增长。另一方面,Chrome和Firefox似乎将内存消耗保持在同一水平。

从用户的角度来看,等待20-30秒并重新加载页面会有所帮助。 Safari会同时清除内存。

编辑:这是一个最小的概念证明,它显示了Safari 12如何在页面加载之间泄漏内存。

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>

复制步骤:

  • 将两个文件都上传到Web服务器
  • 反复单击顶部的链接以在页面之间切换
  • 每次加载页面时,观看Web Inspector的内存消耗都会增加

我向Apple提交了错误报告。将会看到效果如何。

enter image description here

编辑:我将Canvas的尺寸更新为10000x1000,作为更好的概念证明。如果现在将两个文件都上传到服务器上并在iOS设备上运行,则如果您在页面之间快速切换,则在重新加载多个页面后将不会绘制Canvas。如果然后等待30-60秒,则似乎已清除了一些缓存,重新加载后将再次显示Canvas。

答案 4 :(得分:1)

只是想说我们有一个使用Three.js的Web应用程序在iOS 12的iPad Pro(第一代)上崩溃。 升级到iOS 13 Public Beta 7 已修复该问题。该应用程序不再崩溃。

答案 5 :(得分:1)

我已经有很长时间了,但是看来我今天能够解决它。我使用了画布,并在其上多次绘制,没有问题。但是,有时在调整大小后,我遇到了一个异常“总的画布内存使用量超过了最大限制”,并且我的画布似乎已经消失了……

我的解决方案是将画布的大小减小到0,然后删除整个画布。调整大小后,随后初始化一个新画布。

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();
});

不一定需要requestAnimationFrame,但我只想等待设备更新画布。我用iPhone XS Max进行了测试。

答案 6 :(得分:0)

我已经向Apple提交了新的错误报告,但尚未回复。在Google地图中使用折线绘制一条线之后,我添加了执行以下所示代码的功能:(堆栈溢出:为什么这么难包含代码?)

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

查看控制台输出,仅找到4个canvas元素。但是,在Safari调试器中查看“画布”面板时,显示了33个画布(其数量取决于您打开的网页的大小)。
   上面的代码运行之后,画布显示将显示4个较小的画布,这可能是人们期望的。所有其他“孤立”画布仍显示在调试器中。
   我怀疑这证实了“内存泄漏”理论-存在但未包含在文档中的画布。如果超出了画布的内存容量,则无法使用画布渲染任何内容。
   再次,所有这些工作到IOS12为止。我的较旧的运行IOS 10的iPad仍然可以使用。

答案 7 :(得分:0)

另一个数据点:我发现Safari Web Inspector(12.1-14607.1.40.1.4)保留了打开时创建的每个Canvas对象,即使这些对象可能会被垃圾回收。关闭网络检查器,然后重新打开,大多数旧画布将消失。

这不能解决最初的问题-在不运行Web检查器时超出了画布的内存,但是在不知道这个小提示的情况下,我浪费了很多时间走错了路,以为我没有释放任何我的临时画布。