web-dev-qa-db-ja.com

キャンバスの合計メモリ使用量が上限を超えています(Safari 12)

可視化Webアプリケーション に取り組んでいます。これは、d3-forceを使用してキャンバスにネットワークを描画します。各ノードには多くの情報が含まれており、各フレームのすべてを再描画するのはCPUに負荷がかかるため、各ズームレベルに1回、各ノードがキャンバス(DOMにリンクされていない)に描画されるようなキャッシュを実装しました。これらのキャンバスは、ノードの位置でメインキャンバス(DOMにリンク)に描画されます。

結果がメモリ集約型になった場合でも(特に、ピクセル密度が2または3の場合があるhidpiディスプレイ(網膜)で)、速度の向上に満足しています。

しかし今、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のキャンバスを作成すると、すべてうまくいきますが、もう1つ作成すると、canvas.getContextはnullポインターを返します。その場合、別のキャンバスを作成することはできません。

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には、制限がより高い場合(webkitのソースに記載されているように、コンピューターのメモリの4分の1)でも、macOSで同じ問題があります。また、最新のWebkitビルド(236590)を試してみましたが、うまくいきませんでした。ただし、コードはFirefox 62およびChrome 69。

テストコードを改良し、デバッガコンソールから直接実行できるようにしました。誰かが古いサファリ(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)
20
Ogier Maitre

週末を過ごして、問題をすばやく表示できる簡単なWebページを作成しました。 GoogleとAppleにバグレポートを提出しました。ページに地図が表示されます。必要なすべてをパンおよびズームでき、Safariインスペクター(iPadでWebを実行し、MacBook Proを使用してキャンバスを表示)にはキャンバスが表示されません。

次に、ボタンをタップして、ポリラインを1つ描画できます。これを行うと、41個のキャンバスが表示されます。パンまたはズームすると、さらに表示されます。各キャンバスは1MBであるため、孤立したキャンバスが256個あると、iPadのキャンバスメモリがいっぱいになるとエラーが発生します。

ページをリロードし、ボタンをタップして1つのポリゴンを配置すると、同じことが起こります。

同様に興味深いのは、昼と夜のマップをスタイルするボタンを追加したことです。マップ(またはマーカーのみのマップ、マップ上にマーカーを表示するボタンがある)の場合は、前後に移動できます。孤立したキャンバスはありません。しかし、線を引き、スタイリングを変更すると、より孤立したキャンバスが表示されます。

アクティブモニターでMacBookのSafariを見ると、ポリを描画した後、マップをパンおよびズームするとサイズが変化し続けます*

AppleとGoogleがそれを把握し、それが他社の問題であると主張することはないことを望みます。これはすべて、IOS12が長年にわたって安定しており、 IOS 9および10 iPads私はテストのために古いデバイスが現在のWebページを表示できることを確認するために保管しています。このテスト/実験が役立つことを願っています。

6
eepete

誰かが答えを投稿しましたが、これはこの回避策を示しています。アイデアは、キャンバスを削除する前に高さと幅を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)
4
Ogier Maitre

おそらく、最近のWebKitの変更がこれらの問題を引き起こしているはずです https://github.com/WebKit/webkit/commit/5d5b478917c685e50d1032ccf761ca53fc8f1b74#diff-b411cd4839e4bbc17b00570536abfa8f

4
Rathaiah

この問題を確認できます。長年機能していた既存のコードに変更はありません。ただし、私の場合、キャンバスはページがロードされるときに一度だけ描画されます。その後、ユーザーは異なるキャンバス間を閲覧でき、ブラウザはページをリロードします。

これまでのデバッグの試みでは、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のメモリ消費が増加するのを見る

バグレポートをアップルに提出しました。これがどのように機能するかがわかります。

enter image description here

編集:概念のより良い証明として、Canvasの寸法を10000x1000に更新しました。両方のファイルをサーバーにアップロードしてiOSデバイスで実行する場合、ページをすばやく切り替えると、複数のページをリロードした後にキャンバスが描画されません。その後、30〜60秒待機すると、一部のキャッシュがクリアされたように見え、リロードによりキャンバスが再び表示されます。

2
ntaso

別のデータポイント:Safari Web Inspector(12.1-14607.1.40.1.4)は、開いている間作成されたすべてのCanvasオブジェクトを保持します。 Webインスペクターを閉じて再度開くと、古いキャンバスのほとんどが消えます。

これは元の問題を解決しません-ウェブインスペクターを実行していないときにキャンバスのメモリを超過しますが、このちょっとした情報を知らずに、一時的なキャンバスを解放していないと思って間違った道をたどります。

IOS 12のiPad Pro(第1世代)でThree.jsを使用してクラッシュするWebアプリケーションがあると言いたかっただけです。iOS13パブリックベータ7へのアップグレード問題を修正。アプリケーションはもうクラッシュしていません。

1
Shay Cojo

Appleに新しいバグレポートを提出しましたが、まだ返信はありません。 Googleマップでポリラインを使用して線を描いた後、次のコードを実行する機能を追加しました。

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個のキャンバスが表示されていました(開いているWebページのサイズによって異なります)。
上記のコードの実行後、キャンバス表示には、予想されるように、より小さいサイズで検出された4つのキャンバスが表示されます。他のすべての「孤立した」キャンバスは、デバッガーに引き続き表示されます。
これは「メモリリーク」理論を裏付けていると思われます-キャンバスは存在しますが、ドキュメントにはありません。キャンバスのメモリ量を超えると、キャンバスを使用してそれ以上レンダリングできなくなります。
繰り返しますが、これはすべてIOS12まで機能していました。古いiPadを実行しているIOS 10は引き続き動作します。

0
eepete