HHVMの『Incremental Type Checking』を極限まで引き出す:大規模モノレポにおける型チェック最適化の全内幕
我々は長年、数千万行規模のPHPコードベースをHackへと移行させ、そしてHHVM(HipHop Virtual Machine)のランタイム限界と戦い続けてきた。
Hackの最大の武器は、その妥協なき厳格な静的型システム(Strict Mode)にある。動的言語の不確実性を排除し、コンパイル時フェーズで型安全性を担保するこの仕組みは、巨大なコードベースにおいて神々の御業のように機能する。
しかし、コードベースが数百万から数千万行のモノレポ(Monorepo)に達したとき、開発者を絶望させるボトルネックが現れる。それが型チェックのレイテンシーだ。
本稿では、HHVMの型チェッカー(`hh_client` / `hh_server`)が内部でどのようにインクリメンタルな依存関係グラフを構築し、メモリ上で状態を維持しているのか、その低レイヤのメカニズムを解き明かす。そして、この巨獣を手なづけ、CI/CDパイプラインとローカル開発環境を秒速で回すためのアーキテクチャ設計論を叩き込む。
—
1. 内部メカニズム:HHVM Incremental Type Checkerの正体
多くのエンジニアは、型チェッカーを「ファイルを保存するたびに全ファイルを走査するバッチプログラム」だと勘違いしている。だが、HHVMのアーキテクチャは全く異なる。
永続化サーバープロセスと依存関係グラフ(Dependency Graph)
`hh_server`は、起動時にプロジェクト全体をメモリ上に読み込み、巨大なAST(抽象構文木)とシンボル依存関係グラフを構築する。このグラフのノードは「ファイル、クラス、関数、トレイト」であり、エッジは「依存関係(呼び出し、継承、型使用)」を表す。
[File A: BaseController] ──(Extends)──> [File B: AbstractApp]
│
(Type Use)
▼
[File C: UserDTO]
開発者が1つのファイルを変更(`hh_client`が検知、あるいはIDEプラグイン経由)すると、以下のパイプラインがミリ秒単位で発動する。
1. 差分パーシング (Incremental Parsing): 変更されたファイルの差分のみを再パースし、新しいASTを生成。
2. シンボル差分検出 (Symbol Diffing): 旧ASTと新ASTの間で、公開シグネチャ(Public Signatures)に変更がないか比較。
3. 影響範囲伝播 (Dependency Propagation): 変更されたシンボルに依存している下流のファイル群をグラフから逆引きし、再型チェックの対象(Re-check Set)にマーク。
4. 遅延型チェック (Lazy Typechecking): 再マークされたファイルのみを再評価。
なぜ「巨額のメモリ」を消費し、なぜ「遅延」するのか?
`hh_server`が数GBものRSS(Resident Set Size)を消費するのは、この巨大な依存関係グラフと型環境(Type Environment)をRAM上に常駐させているからだ。
ここで問題になるのが、「シグネチャの汚染(Signature Pollution)」である。
もしあなたが、モノレポの根幹にある共通モジュール(例えば `BaseEntity.hack` や `Container.hack`)のパブリックプロパティの型をわずか1つ変更したとする。HHVMの依存関係グラフは、そのモジュールをインポートしている数万のファイルを「無効(Invalidated)」と判定し、事実上のフル再ビルド(Full Recheck)を強制する。
これが、大規模モノレポで型チェックが数分単位でフリーズする根本原因である。
—
2. 依存関係のアンチパターン:巨獣を目覚めさせないための設計
インクリメンタル型チェックの恩恵を最大化するためには、「グラフのエッジを最小化し、影響範囲を局所化する」アーキテクチャが不可欠だ。
アンチパターン:神のコンテナと巨大ファサード
よくある設計ミスは、あらゆるサービスやDTOを1つの巨大なDIコンテナやファサードクラスに集約することだ。
// 【悪夢のアンチパターン】Container.hack
// このファイルを1文字変更するだけで、全モジュールの型チェックが走り出す
namespace App\Core;
class SystemContainer {
// 数百のサービス型がここにハードコードされている
public function getUserService(): UserService { … }
public function getOrderService(): OrderService { … }
public function getPaymentService(): PaymentService { … }
// …あと500行続く
}
このアプローチでは、どれか1つのサービスのメソッドシグネチャを変えただけで、`SystemContainer`に依存するすべてのエンドポイントの型チェックが再実行される。
対策:GenericsとInterfaceによる依存性逆転
Hackの強力な型システムとジェネリクスを使い、依存関係の結合度(Coupling)を劇的に下げる。
// 【推奨される設計】インターフェースとジェネリクスの活用
namespace App\Contracts;
interface IServiceContainer {
public function get
}
// 具象クラスは独立したファイルに隔離し、SystemContainerの肥大化を防ぐ
これにより、個別のサービスを追加・変更しても、コンテナ自体のパブリックシグネチャ(`get
—
3. 実践:厳格モード(Strict Mode)と型チェック最適化のためのHackコード実装
実際のコードベースにおいて、型チェッカーの負荷を軽減しつつ、厳格な型安全性を担保する実践的なコードパターンを見てみよう。
以下は、動的な配列や `mixed` 型の蔓延を防ぎつつ、HHVMの型推論エンジン(Type Inference Engine)が効率的に処理できる構造化されたコードだ。
// strict
hh_strict
namespace App\Optimization;
use type HH\Lib\C;
use type HH\Lib\Vec;
/
- 巨大なデータストリームを型安全かつメモリ効率良く処理するクラス。
- 曖昧な `mixed` や `array` を排除し、ShapeとGenericsで型チェッカーの負担を最小化する。
/
type UserRecord = shape(
‘id’ => int,
‘username’ => string,
‘metadata’ => dict
);
class StreamProcessor
private vec
public function __construct(vec
// HHVMのベクター操作は最適化されており、型チェッカーも高速に推論可能
$this->records = $records;
}
/
- 効率的なフィルタリング。
- クロージャの型を明示することで、型チェッカーの推論コスト(Backtracking)をゼロにする。
/
public function filterActiveUsers(
(function(T): bool) $predicate,
): this {
$filtered = Vec\filter($this->records, $record ==> $predicate($record));
return new static($filtered);
}
public function getRecordCount(): int {
return C\count($this->records);
}
}
チーフアーキテクトの知見:型推論の爆発を防ぐ
上記のコードで特筆すべきは、クロージャの引数と戻り値の型を曖昧にせず、明示的に記述している点だ。
Hackの型チェッカーは強力な型推論機能を持つが、複雑なジェネリクスと無名関数の組み合わせにおいて、型推論が「バックトラック(総当たり的な型探索)」を起こすと、型チェック時間が指数関数的に増加する。
「複雑な式ほど、型を明示せよ。」 これは大規模モノレポで `hh_server` のCPU使用率を100%に張り付かせないための鉄則である。
—
4. モノレポのディレクトリ構造と `.hhconfig` のチューニング
コードの書き方だけではなく、プロジェクトの物理的な配置と設定ファイル(`.hhconfig`)の最適化が、インクリメンタルビルドの生死を分ける。
プロジェクト構造の分離
数百万行のコードを単一のターゲットに押し込むのではなく、明確な境界づけられたコンテキスト(Bounded Contexts)ごとにディレクトリを分割し、緩やかな結合を維持する。
/
├── .hhconfig
├── src/
│ ├── Core/ # 変更頻度の低い基盤層
│ ├── DomainA/ # 独立したドメインA
│ └── DomainB/ # 独立したドメインB
└── tests/
`.hhconfig` の極限チューニング
プロジェクトルートにある `.hhconfig` の設定を見直し、不要な解析をカットする。
.hhconfig の極限最適化例
自動ロード対象外の不要なファイルを完全に除外
ignored_file_patterns = .(Test|Mock)\.hack$
ignored_file_patterns = /vendor/hhvm/.
インクリメンタルチェックの限界を引き上げるためのフラグ
グローバルな共用体型の過剰な拡大を防ぐ
enable_experimental_features = container_strict_keys
並列処理の最適化(利用可能なCPUコア数に合わせる)
hh_server がシステムを窒息させないようスレッド数を制限しつつ最大化
number_of_parked_patch_workers = 4
—
5. 監視とCI/CDパイプラインでの運用戦略
ローカル開発では `hh_server` が常駐してインクリメンタルな恩恵を受けられるが、CI/CD環境(GitHub Actionsや社内Kubernetes基盤など)では、コンテナが毎回クリーンな状態から立ち上がることが多い。
ここで「毎回フルチェック(Full Check)」を走らせていれば、ビルドパイプラインは数分〜数十分でパンクする。
CIでの対策:`hh_client check` と サーバーモードの使い分け
CI環境においては、以下の戦略をとるべきだ。
1. デーモンモードの活用: CIランナーのステップ内で `hh_server` を一度だけバックグラウンド起動(`hh_client` が自動起動)させ、後続のタスクでそのウォームな状態を利用する。
2. Gitの差分に基づく限定チェック: 全ファイルをチェックするのではなく、PR(Pull Request)で変更されたファイルおよびその依存関係のみを対象とする。
CI環境での効率的な差分チェックの例
変更されたファイルリストを取得し、hh_clientに渡す
CHANGED_FILES=$(git diff –name-only origin/main…HEAD | grep ‘\.hack$’)
if [ -n “$CHANGED_FILES” ]; then
# サーバーの生存確認とウォームアップ
hh_client
# 高速なインクリメンタルチェックの実行
echo “Running incremental typecheck on changed files…”
vendor/bin/hh_client
fi
—
結びにかえて
Hackの静的型システムは、正しく飼い慣らせば、巨大なコードベースにおける唯一無二の防壁となる。しかし、その背後で動くHHVMのインクリメンタル型チェッカーの挙動――依存関係グラフの構造、シグネチャの汚染、型推論のコスト――を理解していなければ、システムは自らの重みで崩壊する。
コードの結合度を下げ、型を明示し、アーキテクチャの境界線を守れ。
それこそが、真にスケールするシステムを構築する唯一の道である。