【问题标题】:Total canvas memory use exceeds the maximum limit (Safari 12)总画布内存使用超过最大限制 (Safari 12)
【发布时间】:2018-09-27 08:26:53
【问题描述】:

我们正在开发一个visualization web application,它使用 d3-force 在画布上绘制网络。

但现在我们遇到了 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 问题跟踪器之前确定。

还有其他方法可以破坏画布上下文吗?

提前感谢您的任何想法,指针,...

更新

我发现这个 Webkit 问题(可能)是对这个错误的描述: https://bugs.webkit.org/show_bug.cgi?id=195325

为了添加一些信息,我尝试了其他浏览器。 Safari 12 在 macOS 上也有同样的问题,即使限制更高(计算机内存的 1/4,如 webkit 资源中所述)。我还尝试了最新的 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)

【问题讨论】:

  • 你打算为每个矩形创建一个画布元素吗?
  • 对于这样的可视化,您可能需要查看 webgl... 无论如何,在低端设备上获取如此多的明文 + 缩放 + 平移对于 2d 上下文来说将变得很困难。您可以尝试仅将 sankey 链接保存在画布上,并在每一帧重绘所有节点,这是两个世界的混合,但每个节点的画布确实太多了。 Ps:我怀疑这是硬件限制而不是软件限制。
  • 感谢您的这些想法。在 webgl 中绘制这些东西(例如字体)有点像噩梦。该项目最初使用的是svg,后来修改为使用canvas。所以对我来说,实现这种“缓存”机制而不是 WebGL 更有效。我也不确定硬件限制,因为我认为它之前(在 iOS10 上)工作过,更重要的是,画布没有加载到 DOM 中,所以(据我了解)它们只不过是数据数组。
  • @enxaneta 这段代码几乎只是为了显示我的主要问题。在应用程序中,每个画布都包含一个节点。
  • @OgierMaitre 你有没有找到解决这个问题的办法?

标签: javascript safari html5-canvas webkit mobile-safari


【解决方案1】:

有人发布了一个答案,显示了一个解决方法。这个想法是在删除画布之前将高度和宽度设置为 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)

【讨论】:

  • 我在网络环境中使用谷歌地图。谷歌地图在画布上绘制地图。一切都很好,直到升级到 IOS12。出现“Total Canvas memory use...”错误和“...null 不是读取 a.scale 的对象”错误,并且在几次平移或缩放后一切都中断了。这是我没有更改代码的情况。在运行 IOS 9 和 10 的旧设备上一切仍然有效。IOS 上的 Apple Safari Land 发生了一些变化。带有 Mojave (19.14) 的 MacBook Pro 没有问题。不是答案,但是,希望这可以通过从不同用例中看到类似问题来澄清您的问题。
  • 我刚刚安装了Mojave,所以我明天试试。但我很确定问题仍然存在。你只需要消耗更多的内存。我会告诉你的。
  • 感谢您在旧系统上的测试。
  • 所以,我在 Mojave Safari 上进行了尝试,但问题仍然存在,但正如我所说,在我的计算机上,问题发生在 4GB 左右的画布上,这更难/更长。跨度>
  • 我们也遇到了这个问题。我们在一系列可以滚动的绘图问题中使用画布。这是一个使用 github.com/michaeldzjap/react-signature-pad-wrapper 的 React 应用程序。在componentWillUnmount 中添加if (this._canvas) { this._canvas.height = 0; this._canvas.width = 0; } 似乎已经解决了这个问题。我们无法再收到错误消息,因为我们试图滥用它滚动和重新加载。
【解决方案2】:

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

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

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

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

在活动监视器中查看 MacBook 上的 Safari,当您在绘制多边形后在地图上平移和缩放时,尺寸会保持不变*

我希望苹果和谷歌能够解决这个问题,而不是声称这是其他公司的问题。所有这一切都随着 IOS12 运行多年稳定的网页而改变,并且仍然可以在 IOS 9 和 10 iPad 上运行,我一直在测试以确保旧设备可以显示当前网页。希望这个测试/实验有所帮助。

【讨论】:

  • 你能分享一个链接到你用苹果和谷歌打开的问题吗?
  • 不知道如何分享。苹果还没有任何东西。谷歌正在研究它,但他们怀疑这是苹果的问题。这一切都归结为一旦您创建了多边形或多段线,每次平移或缩放时,Safari 都会显示许多新的画布。最终,您会耗尽画布内存 (IOS) 或占用大量内存。这不会发生在旧版 IOS 或我无法再更新的旧版 MAC Pro 上(感谢 Apple ......)。描述问题就这么简单。
  • Google 已正确判断这是 Apple 的问题 - 在 IOS12 更新的时间范围内,他们的 javascript 库没有任何变化。现在由苹果决定。我向他们发送了系统诊断转储,并等待他们确认问题。错误报告顶部的数字是 45077639
  • 新的 IOS 12.1 确实没有修复了 Safari 中的这个错误。
  • 自 12.3.1 起未修复
【解决方案3】:

另一个数据点:我发现 Safari Web Inspector (12.1 - 14607.1.40.1.4) 会保留在打开时创建的每个 Canvas 对象,即使它们会被垃圾收集。关闭网络检查器并重新打开它,大部分旧画布都会消失。

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

【讨论】:

    【解决方案4】:

    【讨论】:

    • 请在您的回答中添加一些解释以及该提交带来的更改,而不仅仅是链接。
    【解决方案5】:

    我可以确认这个问题。多年来一直有效的现有代码没有变化。但是,在我的情况下,画布仅在页面加载时绘制一次。然后,用户可以在不同的画布之间浏览,并且浏览器会重新加载页面。

    到目前为止,我的调试尝试表明 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 Inspector 内存消耗都会增加

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

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

    【讨论】:

    • 这与我在 IOS12 中看到的类似。如果失败,可以工作多年的代码。不同之处在于我不必重新加载页面,我只需要创建画布然后在同一页面上发布它们。对我来说,这是在您使用 *polys(折线或多边形)执行任何操作时使用 Google Maps javascript 文件时完成的。一个操作显示 40-50 个“孤立”画布,其中 Safari 调试器正在查看 IOS 设备上的网页
    • 这是一个很棒的最小示例。您是否已将此示例发送给 Apple,以便他们了解重现问题的难易程度?
    • @setholopolus 是的,他们关闭了它,因为他们说开销来自开发人员工具。然后我更新了演示并对已关闭的问题写了评论。它在某处说他们会阅读并考虑关闭问题的 cmets。但目前还没有。也许我必须提出一个新问题。当然,这与开发者工具无关(你可以关闭它们,仍然可以看到画布没有被绘制)。内存泄漏确实存在!
    【解决方案6】:

    我有这个问题很长一段时间,但似乎我今天能够解决它。我用了一张画布,在上面画了好几次都没有问题。但是,有时在调整大小后出现“总画布内存使用量超过最大限制”异常,并且我的画布似乎消失了......

    我的解决方案是将画布的大小减小到 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 进行了测试。

    【讨论】:

    • 我可以保证这个 hack,不久前尝试过,它确实修复了问题??
    【解决方案7】:

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

    【讨论】:

      【解决方案8】:

      我已经向 Apple 提交了一份新的错误报告,但还没有回复。在谷歌地图中使用折线画线后,我添加了执行下面显示的代码的功能:

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

      查看控制台输出,只找到了 4 个画布元素。但是查看 Safari 调试器中的“画布”面板,显示了 33 个画布(数量取决于您打开的网页的大小)。
      在上面的代码运行之后,画布显示显示了 4 个尺寸较小的画布,正如人们所预料的那样。所有其他“孤立”画布仍显示在调试器中。
      我怀疑这证实了“内存泄漏”理论——存在但不在文档中的画布。当超出您拥有的画布内存量时,将无法使用画布渲染任何内容。
      同样,所有这些都在 IOS12 之前有效。我运行 IOS 10 的旧 iPad 仍然可以使用。

      【讨论】:

      • 从最新的 12.3.1 版本开始未修复。
      • 虽然它可能是“内存泄漏”,但也可能只是画布对象是在代码中创建的,从未添加到 dom 中。
      • 彼得:同意。这是该问题的链接。令人不安的是,这种情况已经存在 2 年了,谷歌地图团队没有解决这个问题。苹果还声称这不是他们的错误。当技术巨头以牺牲用户为代价发生冲突时……很可能是创建了画布对象,但抛出了一个错误,使它们无法被添加到 DOM 中。查看 URL,他们正在尝试获取错误,以便将其传播回 UserLand。 issuetracker.google.com/issues/142335161
      猜你喜欢
      • 2019-11-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-11
      相关资源
      最近更新 更多