一万手の探偵
30 分の測定 probe —— ライブの 2048 を headless でプレイしてメモリを見る —— が、この Part の探偵譚になった。メモリの問いを訊く前に、もっと悪い発見が届いた: 本番で 1 万手中 9,746 手のキー入力が、誰も覚えていないエラーハンドラに飲まれて silent に死んでいた。手がかりは bounds check を抜けて「&& の両辺を評価していた 3 つ目の backend」へ辿り着き、probe が本来取りに来た数字 —— ゲームの寿命は 64 メガバイト —— で終わった。
二つ目の probe の計画はつつましいものだった: Wasm backend は依然 page-lifetime の割当て —— 何も解放されない —— のままで、ライブの 2048 はプロジェクトが持つ最長走行の Wasm プログラムだ。 headless で 1 万手プレイし、線形メモリを見張り、「ブラウザ backend は手ごとに leak するはず」を 数字に変える。ハーネスは小さい: Node スクリプト、モックの document、実物の DOM glue、合成の 矢印キーイベント。最初の実行は最初のキーで落ちた。メモリの答えが何であれ、何か別のものが 壊れている —— しかも本番で、サイト上で、今まさに。
誰にも見えない故障
最初の教訓はハーネス自身から来た: print import の stub が言語の failure メッセージを黙って 飲み込み、盲目のデバッグに 1 時間を課した —— 使い捨てのハーネスでも、failure 経路は最初に実装 せよ。メッセージが流れ出すと、絵はクラッシュより悪いものに研ぎ澄まされた: ゲームのキーハンドラは trap し、DOM glue はその trap を捕まえ、誰も見ていない console に記録し、続行していた。この 仕組みの下ではゲームは死なない —— 生きているように見えたまま、大半の入力に応えなくなるだけだ。 流し見でなく数えると見出しが出た: 1 万手中 9,746 手が trap。最初の 3 手は動き、その後は ほぼ全滅。ライブのゲームは、数手より先へ進んだ全ての人にとって、公開したその日から壊れていた —— そして公開日の検証(headless の数手)は、まさに「動く数手」だけをテストしていた。dogfood とは 公開時に検証するものではなく、走らせ続けるものだった。
三つ目の backend
記号化したスタックトレース(ビルドフラグ 1 個)はゲームの詰み判定 —— 盤面を走査して「まだ動ける
手はあるか?」と訊くルーチン —— を指した。その内側のテストはプログラミングでもっともありふれた
イディオムだ: r < 3 && bget b (i + 4) == v —— index を guard してから読む。guard は働いていた。
読みは、それでも起きていた。interpreter は && を short-circuit する。C backend は C 自身から
short-circuit を継承する。Wasm backend は両辺を評価していた —— その && は strict なビット
命令にコンパイルされていた —— そして LLVM backend も同罪だと判明した。「MVP サブセットに effect は
無いので eager 評価は観測的に等価」と主張するコメントの陰で。そのコメントはかつて真だった。
trap する配列読みは effect だ。修正は両 backend の &&/|| を既存の if/else emission に
下ろすもので、bounds-guard 付きの読みはこれでどこでも安全になった —— short-circuit の意味を
学んだ 3 つ目と 4 つ目の backend であり、「コメント内の意味論の主張はコードより悪く老いる」への
戒めでもある。
probe が取りに来た数字
ゲームが実際に遊べるようになると、元々の問いが答えを得た。気持ちいいほど正確な答えを。残った trap は全て move 7,764 から始まり、アロケータの bump pointer はモジュールの 64 メガバイトの メモリ天井にぴったり立っていた: ゲームは 1 手あたりおよそ 8.4 キロバイトの回収されないメモリ —— 盤面のスナップショット、描画文字列 —— を燃やし、page-lifetime モデルがページを使い果たすと死ぬ。 熱心なプレイヤーなら 1 時間以内にタブを殺せる。これはゲームのバグではない。回収を持たない backend の正直なコストが、有効数字 3 桁で測られただけだ。測定は forcing number の仕事をした: 「メモリモデルの成果を Wasm に移植する」を「いつかやる」から待ち行列の先頭へ動かし、受け入れ テストまで書き上がっていた —— この同じ probe が、フラットな線を期待して待っている。次の回が それを走らせる。