動的疑似クラス(:hover / :focus / :active)の事前計算を絞る

背景(実測)

style_tree のノード走査は 1 ページあたり約 600 tick(約 6 秒)かかる。 内訳を測ったところ:

index_build=27tick  walk=605tick  nodes=358  rules=2403
candidates_total=17396  (索引なしなら 860274)

セレクタ照合は索引で 2.0%(49 分の 1)まで落ちており、 照合はもうボトルネックではない。時間は照合ループの外にある。

原因が判明した。cascade.rs はノード 1 個につきカスケードを 4 回走らせている:

回 目的 呼び出し
1 通常の算出値 compute_styles_with_vars(..., &[])
2 :hover 時の差分 compute_styles_with_vars(..., &["hover"])
3 :focus 時の差分 compute_styles_with_vars(..., &["focus"])
4 :active 時の差分 compute_styles_with_vars(..., &["active"])

hover_values / focus_values / active_values を事前計算しておく方式のため (マウス移動のたびに再カスケードしないで済むという利点はある)、 4 倍の固定コストが全ノードにかかっている。

仕様

実サイトでは :hover 等を持つルールはごく一部で、 それに該当し得る要素はさらに少ない。

そのノードの候補ルールが該当の状態を一切参照していなければ、 その状態のパスは走らせない。

不変条件

判定の対象

PseudoClass::Other(s) の s が対象の状態名と一致するか。 :hover / :focus / :active はいずれもこの表現で保持される (types.rs の PseudoClass に専用のバリアントは無い)。

以下の入れ子も再帰的に見る必要がある:

:focus-within / :focus-visible は Other に別名として入るため、 focus の判定では前方一致ではなく完全一致にしないと 取りこぼしと過剰検出の両方が起きる。 selector.rs:1238 が active_pseudo_states.contains(&"focus") を :focus-within の判定に使っているため、focus-within / focus-visible を見つけたら focus を参照しているとみなす(D-4 に従い安全側)。

モジュール

css/pseudo_state.rs(純粋・no_std・グローバル状態なし)

pub fn simple_uses_state(s: &SimpleSelector, state: &str) -> bool;
pub fn selector_uses_state(sel: &Selector, state: &str) -> bool;
pub fn rule_uses_state(rule: &Rule, state: &str) -> bool;

実装後の実測

              walk        candidates_total
修正前        605 tick     17396
修正後        291 tick      6022

make test 720 件パス(test_pseudo_state 23 件を新規追加)。

注意: 測定値のばらつき

同一条件でも walk が 291 / 394 / 748 tick とばらつく。 協調スケジューラのため、wall time には他スレッド (TLS ダウンロード)に食われた分が含まれる。 最小値がその時点での実力に最も近い。