可変長Promiseチェーンで発生するメモリリークの防止策
背景
業務上、Promiseチェーンを多く繋いで、大きなデータを処理する必要があった。
これまでメモリリークについて意識することはなかったが、今回の業務における実装では、Promiseチェーンの長さや処理するデータの大きさから、メモリリークが発生する可能性があった。
そのため、Promiseチェーンにおいてどのような書き方をするとメモリリークが発生するのか調査した。
調査
以下のコードは大きなデータを作成し、次の処理へそのデータを渡すような可変長のPromiseチェーンを生成し、処理するコードである。
このコードをブラウザの開発者ツールのConsoleへ直接貼り付けて実行し、動作を確認した。
const tasks = [];
for (let i = 0; i < 1000; i++) {
tasks.push(
Promise.resolve()
.then(() => new Promise(resolve => setTimeout(() => resolve()), 0))
.then(() => {
let a = new Array(9000000).fill('data');
return a;
})
);
}
tasks.reduce((prev, cur, index) => {
return prev.then(() => cur).then(a => {
console.log(index);
return Promise.resolve();
});
});
結果
私の環境下だと、115個程度の処理の時にメモリリークが発生した。
考察
原因を探るために、以下のような次の処理へデータを渡さないパターンを実行して、動作を確認してみた。
このパターンだと、メモリリークは発生しなかった。
このことから、次の処理へデータを渡さない場合はガベージコレクションの対象となり、次の処理へデータを渡す場合はガベージコレクションの対象とならないことが分かる。
もう少し正確に記載すると、次の処理へデータを渡した場合はPromiseが解決されるまでガベージコレクションの対象にはならないということである。
const tasks = [];
for (let i = 0; i < 1000; i++) {
tasks.push(
Promise.resolve()
.then(() => new Promise(resolve => setTimeout(() => resolve()), 0))
.then(() => {
let a = new Array(9000000).fill('data');
return;
})
);
}
tasks.reduce((prev, cur, index) => {
return prev.then(() => cur).then(() => {
console.log(index);
return Promise.resolve();
});
});
上記から得た情報をもとに、次は以下のコードの動作確認を行った。
以下のコードは次の処理へデータを渡すまでは同じであるが、次の処理で明示的にデータの参照を切るようにしたものである。
このコードの結果としては、メモリリークが発生した。
得たデータの参照を明示的に切るようにしても意味はないようであった。
const tasks = [];
for (let i = 0; i < 1000; i++) {
tasks.push(
Promise.resolve()
.then(() => new Promise(resolve => setTimeout(() => resolve()), 0))
.then(() => {
let a = new Array(9000000).fill('data');
return a;
})
);
}
tasks.reduce((prev, cur, index) => {
return prev.then(() => cur).then(a => {
a = null;
console.log(index);
return Promise.resolve();
});
});
データに対して参照を切る操作を行っても改善しないならば、大元のデータの参照を切るのはどうかと考えた。
つまり、tasks[index]
にnull
を直接代入して大元から参照を切るという方法である。
以下のコードは上記を実装したものである。
このコードを実行した結果、コードは最後の処理まで実行することができた。
すなわち、メモリリークは発生しなかった。
これは、tasks
の中で該当のPromiseの参照を保持し続けているためにGC対象とならなかったのだが、tasks[index]
にnull
を直接代入したことにより、該当のPromiseやデータの参照が完全になくなったことでGC対象になったのだと推測できる。
const tasks = [];
for (let i = 0; i < 1000; i++) {
tasks.push(
Promise.resolve()
.then(() => new Promise(resolve => setTimeout(() => resolve()), 0))
.then(() => {
let a = new Array(9000000).fill('data');
return a;
})
);
}
tasks.reduce((prev, cur, index) => {
return prev.then(() => cur).then(a => {
tasks[index] = null;
console.log(index);
return Promise.resolve();
});
});
結論
以下のようなコードにすることでメモリリークを防止することができる。
const tasks = [];
for (let i = 0; i < 1000; i++) {
tasks.push(
Promise.resolve()
.then(() => new Promise(resolve => setTimeout(() => resolve()), 0))
.then(() => {
let a = new Array(9000000).fill('data');
return a;
})
);
}
tasks.reduce((prev, cur, index) => {
return prev.then(() => cur).then(a => {
tasks[index] = null;
console.log(index);
return Promise.resolve();
});
});
まとめ
可変長のPromiseチェーンにおいて、メモリリークが発生する実装方法を調査し、メモリリークを発生させない対処法を考察した。
配列を用いた可変長のPromiseチェーンにおいては、処理が完了したPromiseの処理にnull
を代入することで参照を切り、ガベージコレクションの対象とすることができる。
それにより、メモリリークの発生を防ぐことが可能となる。
Discussion