<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>パフォーマンス</title><link>https://blog.ast.moe/tags/%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9/</link><description>Recent posts from 五里霧中</description><generator>Hugo</generator><item><title>OmoikaneのRealm生成が遅かったので、Boaのscope検索と字句解析を調べた</title><link>https://blog.ast.moe/blog/2026-09-29-2/</link><pubDate>Tue, 29 Sep 2026 18:25:00 +0900</pubDate><guid>https://blog.ast.moe/blog/2026-09-29-2/</guid><description>&lt;p&gt;&lt;a href="https://blog.ast.moe/blog/2026-09-29/"&gt;OmoikaneのCIの待ち時間を調べた記事&lt;/a&gt;では、文書だけの変更に重い検証を走らせないことや、ビルドキャッシュを見直した話を書いた。それとは別に、実行する必要のあるテストそのものにも時間がかかっていた。&lt;/p&gt;
&lt;p&gt;OmoikaneはJavaScriptの実行にBoaを使っている。ページやiframeのためにJavaScriptのRealmを作るたび、DOM APIを用意する大きなスクリプトを実行する。その処理がテスト時間の相当部分を占めていた。&lt;a href="https://github.com/ieee0824/omoikane/issues/1154"&gt;Issue #1154&lt;/a&gt;では、テストの数やRealmの数を減らすのではなく、この初期化そのものを調べた。&lt;/p&gt;
&lt;p&gt;先に結果を書くと、macOSのtest profileでRealm生成時のbootstrap処理は約0.58秒から約0.13秒になった。ただし、これはrelease buildのページ表示時間を測った結果ではない。何を測り、どこを変えたらそうなったのかを順に残しておく。&lt;/p&gt;
&lt;h2 id="realmを作るたびに何が起きていたか"&gt;Realmを作るたびに何が起きていたか&lt;/h2&gt;
&lt;p&gt;RealmはJavaScriptの組み込みオブジェクトやグローバル環境を持つ実行単位で、Omoikaneではページやiframeごとに必要になる。Realm自体については&lt;a href="https://blog.ast.moe/blog/2026-09-25-3/"&gt;以前の記事&lt;/a&gt;に書いた。&lt;/p&gt;
&lt;p&gt;新しいRealmを作ると、OmoikaneはDOMをJavaScriptから使えるようにするbootstrapを読み込む。&lt;code&gt;dom_bootstrap.js&lt;/code&gt;、&lt;code&gt;xpath.js&lt;/code&gt;、&lt;code&gt;font_loading.js&lt;/code&gt;、&lt;code&gt;find_in_page.js&lt;/code&gt;を連結したソースは約1.1MBある。&lt;a href="https://github.com/ieee0824/omoikane/blob/main/src/js/mod.rs"&gt;&lt;code&gt;evaluate_dom_bootstrap&lt;/code&gt;&lt;/a&gt;はそのソースをmoduleとして解析し、load、link、evaluateまで進める。内部のhost bindingをページのglobalに露出させていないかも、この初期化時に確認している。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Realmを作る
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bindings.module_source() 約1ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Module::parse 約335ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;module.load + link 約240ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;module.evaluate 約10〜25ms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これは変更前に一回だけ実行したときの、おおよその内訳である。全体で約0.58秒かかる。特に&lt;code&gt;parse&lt;/code&gt;と&lt;code&gt;load&lt;/code&gt;・&lt;code&gt;link&lt;/code&gt;が重かった。bootstrapソースは毎回ほぼ同じなのに、Realmごとに解析とコンパイルを繰り返している。&lt;/p&gt;
&lt;p&gt;一時的な計測コードを入れてlibのユニットテストを全件走らせると、1069件のテストからbootstrapが計1414回呼ばれていた。0.58秒を掛けると約820秒になる。libテストのCPU時間は約1160秒だったので、概算で7割ほどがこの初期化に対応する。一方、実時間は160〜222秒だった。テストが並列に走るため、CPU時間と時計で測った実時間は同じ値にはならない。&lt;/p&gt;
&lt;p&gt;実際、Realmを25個作るテストは単体で16.1秒、13個作るテストは7.8秒かかっていた。遅いテストを一つだけ削っても、根本の費用は残る。そこで「bootstrapを何度も処理しない」「処理するなら処理自体を速くする」の両方を検討した。&lt;/p&gt;
&lt;h2 id="最初の案は解析済みmoduleのキャッシュだった"&gt;最初の案は解析済みmoduleのキャッシュだった&lt;/h2&gt;
&lt;p&gt;最初に立てた&lt;a href="https://github.com/ieee0824/omoikane/issues/1155"&gt;子Issue #1155&lt;/a&gt;は、解析済みのbootstrap moduleをプロセス内でキャッシュするというものだった。同じソースを何度も&lt;code&gt;Module::parse&lt;/code&gt;へ渡すのだから、一回作ったASTを使い回せれば分かりやすい。&lt;/p&gt;
&lt;p&gt;ただ、Boaの解析結果をそのままスレッド間で共有できるわけではない。ASTの&lt;code&gt;Scope&lt;/code&gt;は&lt;code&gt;Rc&lt;/code&gt;と&lt;code&gt;RefCell&lt;/code&gt;を含み、ASTのシンボルは&lt;code&gt;Context&lt;/code&gt;ごとのInternerにも依存している。libtestはテストごとに別スレッドで動く。測定した1414回のうち、同じスレッドで二回目以降のRealmを作ったのは345回だけだった。残りの大半には、スレッド内だけのキャッシュを置いても効かない。&lt;/p&gt;
&lt;p&gt;「キャッシュすればよい」という方向で実装を進める前にプロファイルを取ると、別の問題が見えた。解析とコンパイルの上位に、&lt;code&gt;JsString&lt;/code&gt;の比較がいた。呼び出し元は、Boaのscope解析やbytecode生成で行うbindingの名前検索だった。&lt;/p&gt;
&lt;h2 id="大きなscopeを何度も線形探索していた"&gt;大きなscopeを何度も線形探索していた&lt;/h2&gt;
&lt;p&gt;JavaScriptの変数や関数の名前を解決するとき、パーサーとコンパイラはscopeのbindingを調べる。Boaでは、そのbindingを宣言順の&lt;code&gt;Vec&lt;/code&gt;に持ち、名前を探すたびに先頭から比較していた。小さな関数のscopeならこれで十分だが、DOM bootstrapのmoduleトップレベルには大量のbindingがある。&lt;/p&gt;
&lt;p&gt;名前の参照が増えるたびに、長いリストをまた走査する。binding数を&lt;code&gt;B&lt;/code&gt;、参照数を&lt;code&gt;R&lt;/code&gt;とすれば、この部分の仕事はおおむね&lt;code&gt;R × B&lt;/code&gt;に増える。約1.1MBのソースをRealmごとに処理すると、その費用を毎回払うことになる。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/ieee0824/omoikane/pull/1160"&gt;PR #1160&lt;/a&gt;では、宣言順の&lt;code&gt;Vec&lt;/code&gt;は残し、名前からその位置を引ける&lt;code&gt;FxHashMap&lt;/code&gt;を追加した。ただし、すべてのscopeに最初からハッシュ表を持たせたわけではない。bindingが16件以下なら従来どおり線形探索し、17件目を追加したときに索引を作る。普通の小さなscopeで索引の確保費用が増えるのを避けるためだ。&lt;/p&gt;
&lt;p&gt;bindingは追加されるだけで、削除や改名はしない。そのため索引に入れた&lt;code&gt;Vec&lt;/code&gt;上の位置は後から無効にならない。宣言順、binding index、外側のscopeからの参照、再宣言、escapeの扱いは変えない。しきい値の前後でこれらが変わらないこともテストした。&lt;/p&gt;
&lt;p&gt;結果は、単なる&lt;code&gt;parse&lt;/code&gt;の短縮にとどまらなかった。名前検索はscope解析だけでなくbytecode生成でも使われるため、&lt;code&gt;load&lt;/code&gt;・&lt;code&gt;link&lt;/code&gt;も大きく短くなった。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;処理・計測範囲&lt;/th&gt;
&lt;th style="text-align: right"&gt;変更前&lt;/th&gt;
&lt;th style="text-align: right"&gt;scope索引化後&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bootstrapの&lt;code&gt;Module::parse&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;約335ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;約115ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bootstrapの&lt;code&gt;load&lt;/code&gt; + &lt;code&gt;link&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: right"&gt;約240ms&lt;/td&gt;
&lt;td style="text-align: right"&gt;約25ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libユニットテストのCPU時間&lt;/td&gt;
&lt;td style="text-align: right"&gt;1160秒&lt;/td&gt;
&lt;td style="text-align: right"&gt;427秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libユニットテストの実時間&lt;/td&gt;
&lt;td style="text-align: right"&gt;160〜222秒&lt;/td&gt;
&lt;td style="text-align: right"&gt;63秒&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;これはmacOS 8コアのtest profileでの測定である。libテストの実時間に幅がある変更前の値を使っているため、単一条件の厳密な倍率として読まない方がよい。ただ、解析だけをキャッシュする案では残っていたはずのコンパイル側まで改善したのは大きい。&lt;/p&gt;</description></item></channel></rss>