【上級者向け】HHVM型チェッカーのIncremental Checkの最適化:大規模モノレポにおける型チェック時間の短縮術
数百万行を超えるHackのコードベースを管理するシニアエンジニアやアーキテクトであれば、一度は絶望したことがあるはずだ。全社規模のモノレポにおいて、CI/CDパイプラインやローカルでの`hh_server`の応答が数分単位でフリーズする現象——いわゆる「型チェックの地獄」だ。
我々はPHPの動的な泥沼から抜け出すためにHackを選び、厳格な静的型システム(Strict Mode)とHHVMのJITコンパイラがもたらす圧倒的なパフォーマンスを手に入れた。しかし、コードベースが肥大化するにつれて、型チェッカー(`hh_client` / `hh_server`)そのものが開発体験のボトルネックへと変貌する。
本稿では、HHVM型チェッカーの内部挙動、インクリメンタルチェック(Incremental Check)のメモリ構造、そして数百万行のスケールでも数秒で型チェックを完了させるためのアーキテクチャ的最適化手法を、極限の低レイヤ視点から解き明かす。
—
1. HHVM型チェッカーの内部構造と依存グラフ(Dependency Graph)
まず、敵を知るためにHHVM型チェッカー(`hh_server`)がメモリ上で何を行っているかを理解しなければならない。
`hh_server`は、起動時にファイルシステム全体をスキャンし、AST(抽象構文木)を構築した上で、グローバルな依存グラフ(Dependency Graph)をRAM上に構築する。このグラフのノードはクラス、関数、トレイト、typedefであり、エッジはそれらの依存関係(継承、メソッド呼び出し、型ヒントなど)を表している。
インクリメンタルチェックの罠
開発者が1つのファイルを保存した瞬間、`hh_server`は以下のフェーズを実行する。
1. File Watcher (inotify等) による変更検知
2. Re-parsing: 変更されたファイルのAST再構築
3. Re-typechecking (Dependent Propagation): 変更されたシンボルに依存しているすべてのダウンストリームノードの型再検証
ここで発生するのが 「カスケード再計算(Cascade Re-evaluation)」 である。例えば、コアな基底インターフェースや、数千のクラスで使われている共通の型エイリアスを1行変更したとする。型チェッカーは依存グラフを辿り、モノレポ全体の大部分を「ダーティ(汚染された)」とマークし、結果的にフルビルドに近いコストを支払うことになる。
—
2. ボトルネックの特定:プロファイリングとメモリ管理
型チェックが遅いと感じたとき、感覚で設定をいじっても無駄だ。HHVMには型チェッカーの内部メトリクスを測定するためのフラグが用意されている。
hh_clientにプロファイリング情報を要求する
hh_client –profile
出力されるJSONやログの中で注目すべきは以下の指標だ。
- `heap_size`: `hh_server`が消費しているRSS(Resident Set Size)。これが物理メモリを超過するとOSのOOM Killerかスワップ地獄によりパフォーマンスが崩壊する。
- `dependency_graph_edges`: エッジの総数。これが数十万を超えると、差分計算のオーバーヘッドが線形を超えて増大する。
- `lazy_check` のヒット率: 遅延型チェックがどれだけ機能しているか。
メモリ空間の最適化:Shared MemoryとOcamlのGC
`hh_server`はOCamlで実装されている。OCamlのガベージコレクション(GC)は、大規模なASTと型環境(Type Environment)を維持する際、マイナーヒープとメジャーヒープの間で膨大なポインタ追跡を行う。
モノレポの規模が大きくなると、GCの停止時間(Stop-the-world)が数秒に達することがある。これを緩和するには、OSレベルでの大ページメモリ(Huge Pages)の有効化や、OCamlのGCチューニング環境変数を`hh_server`の起動スクリプトに仕込む必要がある。
例: OCamlのGCパラメータチューニング(hh_server起動前)
export OCAMLRUNPARAM=”s=8M,i=1,h=3″
—
3. 大規模モノレポにおける型チェック最適化のアーキテクチャ戦略
コードベースの分割や設定だけでは限界がある。シニアエンジニアが実践すべき具体的なアーキテクチャ戦略を提示する。
戦略 A: 境界の厳格化と「型バウンダリー(Type Boundaries)」の設定
多くのモノレポで起きる失敗は、すべてのコードが一つの巨大な`.hhconfig`の管理下にあることだ。これでは、末端のフィーチャーコードの変更が、フレームワーク層の型定義の再検証を誘発する。
これを防ぐためには、プロジェクトを論理的な「パッケージ」または「モジュール」に分割し、`.hhconfig`を階層的に配置する。
/
├── .hhconfig (ルート: グローバル設定)
├── src/
│ ├── Core/
│ │ ├── .hhconfig (独立した型境界)
│ │ └── …
│ └── Features/
│ ├── FeatureA/
│ │ ├── .hhconfig
│ │ └── …
各モジュールの境界(Public API)では、具象型ではなく厳格なインターフェース(Interface)またはopaque type(不透明型)を使用し、内部の実装詳細が外部に漏れ出さないようにする。これにより、内部実装を変更しても、依存グラフのエッジが外側に伝播するのを防ぎ、インクリメンタルチェックの範囲を最小限に抑えられる。
戦略 B: Hackの不透明型(Newtypes / Opaque Types)による依存の断ち切り
依存関係の迷宮化を防ぐ最も強力なHackの言語機能が、`newtype`(不透明型)だ。
以下のコードを見てほしい。通常の型エイリアスを使用すると、型チェッカーは内部のプリミティブ構造まで追跡してしまう。
// 悪い例: 単なる型エイリアス(Type Alias)
// 依存グラフが結合し、変更時の影響範囲が広がる
type UserId = int;
type OrderId = int;
function process_order(users.UserId $u, orders.OrderId $o): void { … }
これを`newtype`に変更する。
// 良い例: 不透明型(Opaque Type)
// モジュール境界の内側と外側で型を完全に隔離する
namespace MyCompany\Domain;
newtype UserId = int;
class UserIdentity {
public static function create(int $id): this->UserId {
return $id;
}
}
不透明型を使うことで、型チェッカーは「モジュール外からは単なるopaqueな型」として扱い、内部構造の変更によるカスケード再計算の連鎖を断ち切ることができる。
—
4. 現場で使える実践的最適化コードと設定
最後に、今すぐCIやローカル開発環境(LSP)に適用できる具体的な `.hhconfig` の最適化レシピと、チェッカーの負荷を下げるコードイディオムを示す。
1. `.hhconfig` の最適化チューニング
ルートディレクトリの `.hhconfig` に以下のディレクティブを設定し、不要なチェックや過剰な依存追跡を抑制する。
; .hhconfig の極限最適化例
assume_php = false
allowed_decl_fixme = false
; 自動オートロードの範囲を制限し、無駄なファイルのパースを防ぐ
autoloader_enabled = true
; 無視するディレクトリ(テストモックや自動生成コードなど、型安全性が厳密でなくてもよい領域)
ignored_paths[] = “vendor/.”
ignored_paths[] = “generated/.”
; 実験的だが大規模環境で劇的な効果を発揮するフラグ群
enable_xhp_class_modifier = true
glean_indexing = false
2. ダイナミック型(`mixed` / `dynamic`)の局所化によるグラフ枝刈り
「すべてをStrict Modeにする」ことは美しい理想だが、サードパーティライブラリとの統合部やレガシーコードの境界において、型チェッカーに無限の推論コストを支払わせる原因になる。
境界部分ではあえて `dynamic` または慎重な型アサーションを用い、依存グラフの暴走を意図的に「カット」する。
namespace MyCompany\LegacyAdapter;
// レガシーな外部配列を安全にラップし、型チェッカーの推論爆発を防ぐ
class LegacyBridge {
public static function unwrapUnsafeData(mixed $raw_data): shape(‘id’ => int, ‘name’ => string) {
// 境界で明示的なランタイムキャスト(HH\Asioや形状チェック)を行い、
// 静的型チェッカーの推論コスト(推論の深さ)をここで打ち切る。
$id = Shapes::idx($raw_data, ‘id’, 0);
$name = Shapes::idx($raw_data, ‘name’, ”);
invariant(is_int($id) && is_string($name), ‘Invalid shape at boundary’);
return shape(‘id’ => $id, ‘name’ => $name);
}
}
アーキテクトの視点: 上記のように境界で明示的なガード(`invariant`等)を入れることで、型チェッカーはそれより上流・下流の複雑な型推論ツリーを保持・再計算する必要がなくなる。結果として、メモリ消費量が劇的に削減される。
—
5. 結び:型システムの恩恵を殺さずに速度を支配せよ
Hackの静的型チェッカーは、正しく飼い慣らせば最強の開発加速装置だが、無秩序なコードベースに適用すれば、開発者の足を引っ付く重荷へと変わる。
1. 依存グラフの構造を意識し、`newtype`やモジュール分割でエッジを遮断する。
2. `.hhconfig` とファイル監視のスコープを適切に制限する。
3. 型チェッカーの推論爆発ポイントを境界(Boundary)で意図的にカットする。
これらの低レイヤのメカニズムを理解し、アーキテクチャレベルで制御することこそが、真にスケーラブルなHackモノレポを構築唯一の道である。コードベースの重みに屈するな。アーキテクチャの力で、型チェッカーを従えろ。