11月じゃん…。スーガクちょっとやって仕事の実装。芋煮作る。里芋剝くのが面倒くさすぎる。味が薄すぎる。
-
恥ずかしい話。Tone.js を使って、6個ぐらいの mp3 ファイルを直列に読み込んでいた。
-
メモリが貧弱な Android 端末でクラッシュした。
- 端末を PC と接続して、Chrome のプロファイラを見た。
- Performance タブは正常、Memory タブを見ても JS Heap は 10 MB ぐらいしか使っていない。どう見てもメモリ不足で落ちてるんだけど、原因をはっきり特定できない状態。
- mp3 ファイルも最大で 10MB 程度しかないので、仮に全部メモリに乗ったとしてもそんなに問題ない感じ。
adb shell top -d 1でメモリの使用状況を確認した。- すると、クラッシュするまで継続的に 1GB ぐらい全体のメモリ使用量が増大し続けていた。
- 調べたところ、Tone.js が依存している
decodeAudioData()はメモリをかなり消費することがわかった。decodeAudioData()は、圧縮された MP3 ファイルを非圧縮の PCM データに展開するもの。PCM データのサイズはサンプリングレートが 44,100Hz だとしたら、デフォルトが 32bit PCM、2ch なので 44100 * 4 * 2 = 352,800 B/秒。したがって、5分の長さのファイルをデコードするのに少なくとも 100MB 以上のメモリを確保する必要があることになる。- 一時的に 100MB 必要になるのならまだ許せる。問題はなぜデコード処理を何回も連続して呼び出すと 1GB 消費してしまうかだが、デコード処理はネイティブのレイヤーで行われるため、実行エンジンによる GC のタイミングと厳密に同期しない。そのため、非同期処理が見かけ上終了して AudioBuffer が参照されなくなっても、確保されたメモリがちょっとだけ居残ってしまうのではないか。
- ところで、なんで 1GB もメモリをデコードに使ってたのに、 プロファイラの Peformance タブ、Memory タブにそれが反映されてなかったのか?
- 端末を PC と接続して、Chrome のプロファイラを見た。
-
そもそも誤解していたんだけど、ざっくりブラウザにはネイティブメモリとJS Heap、二種類のメモリ領域があるっぽい(超ざっくり)。
- Chrome のタスクマネージャを開くと、“Memory” と “JavaScript Memory” 、2つのカラムがあって、前者がネイティブメモリ、後者が JS Heap に対応している。ネイティブメモリの使用状況は Peformance, Memory タブでは確認できない。確認できるのは JS Heap の内容だけ。つまり、JavaScript の配列、オブジェクト、関数、ArrayBuffer、TypedArray とか。
- Performance API (
performance.memory()やperformance.measureUserAgentSpecificMemory()) も JS Heap の情報しか取得できない。 - 一方で
decodeAudioData()みたいな 、JavaScript と別スレッドで動くネイティブの処理には、当然ネイティブメモリが使われることになる。つまり、デコードのための一時バッファなど。 - このことを知ってからタスクマネージャでページを確認すると、やはり 800MB ほどネイティブメモリをデコードに使っていた…。使いすぎィ!
- ネイティブメモリの中身を見る方法ってないのかな、 chrome://tracing とかでわかるんか?
-
【学び】メモリの貧弱なモバイル端末では、ネイティブメモリの使用量も意識しなければならない / タスクマネージャを見なければわからないこともある(戒め)