概要
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);
}
この対応の問題点
- 全てのキーと値がメモリ上に載る
-
getAllKeys
とgetAll
の結果 (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 がヤバい。
