3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Safari で Indexed DB の IDBCursor.onsuccess 引数のメモリに関する問題

Last updated at Posted at 2018-07-06

概要

Safari において、Indexed DB 利用時に IDBCursor の onsuccess コールバック引数のプロパティ target.result を参照した場合、それが null でない場合に特殊なメモリリークが発生した。
上記条件下でブラウザページをリロードした場合、javascript オブジェクトがゾンビ化してメモリ領域に残り続ける。
オブジェクトはリロードの回数に応じて増え続けるため、メモリ圧迫によるブラウザページクラッシュを引き起こす可能性がある。
Chrome ではこの現象は確認できなかった。

尚、当該現象は既に Apple Bug Reporter を通じて報告済みである。

現象を確認した環境

  • maxOS 10.12.6 / Safari 11.1.1
  • iOS 10.3 / Safari
  • iOS 11.4 / Safari

検証

検証用のサンプルコード。

// 正常に indexed db が接続できているとする
var transaction = openRequest.result.transaction([storeName], 'readwrite');
var store = transaction.objectStore(storeName);

// 何かレコードを保存していないと、後述のプロパティが null となる
store.put(key, 1);

store.openCursor().onsuccess = function(e) {

    // 1. e.target.result が null の場合
    // var result = e.target.result;

    // 2. e.target.result が null ではない場合
    // var result = e.target.result;
    
    // 3. e.target の参照のみ行う
    // var target = e.target;
    
    // 4. e.target の参照のみだが関数内で展開
    // console.log(e.target);

};

結果

case result
1. e.target.result が null の場合 リロードによるリークは発生しない
2. e.target.result が null ではない場合 リロードでリークする
3. e.target の参照のみ行う リロードによるリークは発生しない
4. e.target の参照のみだが関数内で展開 リロードでリークする

考察

通常の Webページ/サービスであれば対して問題になりにくいが、大きなバイナリなどを扱うページの場合はクラッシュのリスクが高い。
メモリ解放手段としてページのリロードが行われることもあるが、この不具合が発生する条件下では逆効果である。

対応方法 (暫定)

カーソルの用途はいくつかあるが、全てのレコードをイテレートする可能性のある処理を行うものと仮定した場合、getAllKeys 及び getAll が代替となる。
getAll 等は indexeddb2 の仕様であり、比較的新しいバージョンのブラウザでのみサポートされている。

W3C
https://www.w3.org/TR/IndexedDB-2/
caniuse
https://caniuse.com/#feat=indexeddb2

対応例 1

var allKeys;
var allValues;

objectStore.getAllKeys().onsuccess = (event) => {
    allKeys = event.target.result;
};
objectStore.getAll().onsuccess = (event) => {
    allValues = event.target.result;
};

...

for (var i = 0; i < allKeys.length; i++) {
    const key = allKeys[i];
    const val = allValues[i];
    doSomething(key, value);
}

この対応の問題点

  • 全てのキーと値がメモリ上に載る
  • getAllKeysgetAll の結果 (event.target.result) の並び順が保証されている前提である

対応例 2

var allKeys;
objectStore.getAllKeys().onsuccess = (event) => {
    allKeys = event.target.result;
};

...

for (var i = 0; i < allKeys.length; i++) {
    const key = allKeys[i];
    objectStore.get(key).onsuccess = (event) => {
        const value = request.result;
        doSomething(key, value);
    };
}

この対応の問題点

  • 全てのキーがメモリ上に載る
  • キー毎にDBを読むのでパフォーマンス面での懸念が多い

おまけ

始めて見た時はひいた。
window はいいけど、その下の ArrayBuffer, AudioBuffer がヤバい。

omake.png
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?