Edited at

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


概要

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


検証

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

https://github.com/dolow/safari-idb-memory-leak-sample

// 正常に 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