https://zenn.dev/b_inary/articles/9cbe5a1550edca#safari%3A-web-worker-の内部で-web-worker-が生成できない

WebAssembly って実際どうなのよ

踏み抜いたバグたち

ここからは、開発をしている際に踏み抜いたバグたちをいくつか紹介していきます。

Safari: Web Worker の内部で Web Worker が生成できない

Safari では Web Worker の内部で Web Worker を生成することができません (Bugzilla) 。Bugzilla で最初に報告されたのが2008年ですから、13年以上もの間このバグが放置されていることになります。対応が非常に大変なのであろうことは理解しますが、2022年になっても対応していないというのはどうなのでしょうか。

今回利用している wasm-bindgen-rayon ライブラリでは、メインスレッドはボスとなる Worker を生成し、このボス Worker が子分となる Worker をスレッド数の分だけ呼び出すようなモデルを使えと言っています。しかし Safari ではボス Worker が子分 Worker を呼び出せないということになりますから、このモデルが動作しません。技術的にはメインスレッドがスレッド数の分の Worker を直接管理することも可能なのかもしれませんが、wasm-bindgen-rayon を捨てるか、Safari を捨てるかの選択で私は Safari の方を捨てることにしました。そもそも wasm-bindgen-rayon の方を捨てたとして Safari が救えるのかどうか自信がありません。

一応、Safari を捨てたといっても Safari ではボス Worker が子分を呼ばずに自らが計算を行うようにすることで、シングルスレッドでは動作するよう本当に最低限の対応は行っています。とはいえソルバーがマルチスレッドで動かないというのは致命的ですから、macOS ユーザーの方は Chrome か Firefox を使ってくださいとしか言えません。iOS ユーザーの方はもうどうしようもないですが、モバイル端末の計算能力はたかが知れていますしサポート対象外とさせていただくことにします。

Firefox: 有効な Web Worker が GC に回収されてしまう

Firefox には Firefox で、有効な Web Worker を数秒放置すると GC に回収されてしまうことがあるというバグがあります (Bugzilla) 。こちらは workaround として有効な Web Worker を明示的にグローバル変数などに保存しておけば対処は可能で、実際 wasm-bindgen-rayon はそのように対応しています。

さて問題はここからで、せっかく Worker をグローバル変数に保存するよう対応しているのに、それらは未使用な変数だからとバンドラが最適化で削除してしまうことがありました。具体的には、一番最初のプロトタイプで採用していた Next.js がそうでした。恐らく最適化オプションのどこかをいじれば防げるのでしょうが、その原因を特定する気力も技術力も無く、これが原因で Next.js はお蔵入りになりました。まあ結局は先述したように React すら使わないことになったのですが。

Chrome: 仮想スクロールを実装した要素でスクロールイベントが発火しない

計算結果画面には各ハンドのエクイティや EV、各アクションを選択するべき確率といった詳細を表示するテーブルがあるのですが、このテーブルは最大で1000行を超えることになり、描画に掛かる時間が馬鹿にならないため仮想スクロールを自前で実装しています。ちなみに仮想スクロールとは、巨大なリストなどにおいて現在見えている要素とその周辺の要素のみをレンダリングし、見えていない要素は極力レンダリングを行わないようにすることで、描画のパフォーマンスの改善を図るテクニックのことです。なお、なぜ既存のライブラリを使わず自前で実装したのかというと、単に <table> 要素で使える良さげなライブラリが見つからなかったからです。

この仮想スクロールを実装した要素においてマウスのホイールを連続して回してスクロールしていると、Chrome では途中からスクロールイベントがなぜかまったく発火しなくなるというバグを踏みました。当然スクロールそのものも行われなくなり、ホイールをいくら上下に回してもうんともすんとも言わない状態となります。マウスカーソルを少しでも動かすか、1秒ほどホイールを回さずに待機すると再びスクロールできるようになるのですが、これでは地味に結構不便です。

これに関しては今でも原因がまったく分かっていないのですが、paint complexity なるものが関わっているのではというコメントを見つけ、overflow-y: scroll; を指定している <div> 要素に will-change: transform; という CSS プロパティを設定してみたところ、なんと症状が解決しました。完全に魔術です。なお自分の環境ではこれで解決したのでヨシ! ということでコミットしてしまいましたが、残念ながらデベロッパーツールを開くと症状が復活するんですよね。この件に関してなにか情報をお持ちの方がいらっしゃいましたらコメントいただけると大変助かります。

wasm-bindgen: メモリ使用量が 2GB を超えると想定外のアドレスを参照する

このバグに関してはいかにもありそうな感じのバグですよね。WebAssembly はアドレス空間が32ビットなので 4GB までのメモリを扱えるのですが、wasm-pack (wasm-bindgen-cli) が出力する JavaScript のグルーコードの中にアドレスを符号付き32ビット整数として取得している処理があり、そんなことをすると当然 2GB を超えるアドレスは負になってしまってバグります。

一旦アドレスが負になってしまっても、それを渡された側が再び符号無し32ビット整数として解釈してくれればバグにはならないのですが、JavaScript の TypedArray 型の subarray() は負のインデックスを渡されると配列の末尾からの位置として解釈して特にエラーも返さないので、単にバグった位置のデータが普通に返ってしまいます。

Wasm でメモリ使用量が 2GB を超えるアプリケーションなんてものは相当珍しいのでしょう。私は出力されたグルーコードを適当に sed で置換して対応していますが、可能なら大元のバグが直接修正される方が望ましいですから、contribution が好きな方がいればぜひ issue でも立ててきてくださると救われる人がいるかもしれません。私としては sed で対応できているのでアクションを起こすモチベーションがあまりありません。