【テクニカル・上級編】HHVM型チェッカーの『Incremental Check』を高速化するコード分割のアーキテクチャ – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

Hackを掌握する極限の知見:HHVM型チェッカーの『Incremental Check』を極限まで加速するコード分割のアーキテクチャ

メタ社がPHPの動的柔軟性を捨ててまで構築したHack言語とHHVM(HipHop Virtual Machine)。その核心にあるのは、コンパイル時における妥協なき厳格な静的型システム(Strict Mode)と、瞬時に走る型チェッカー(`hh_client` / `hh_server`)の圧倒的なフィードバックループだ。

しかし、コードベースが数百万行を超える規模に達したとき、開発者を絶望させるボトルネックが訪れる。それが「インクリメンタル・チェック(Incremental Check)の停滞」だ。

今回は、HHVMの型チェッカーが内部でどのように依存関係グラフ(Dependency Graph)を構築・評価しているのかという低レイヤのメカニズムに踏み込み、その限界を突破するためのモジュール分割とアーキテクチャ設計の極意を授ける。

—

1. 内部メカニズム:HHVM型チェッカーのライフサイクルと依存グラフ

型チェッカー(`hh_server`)は、デーモンとして常駐し、メモリ上に巨大なAST(抽象構文木)のインメモリ・データベースと依存関係グラフ(Edge Graph)を保持している。

開発者がファイルを1つ保存した瞬間、以下のパイプラインが爆発的な速度で走る。

1. ファイルウォッチャー検知: 変更されたファイルの差分を特定。
2. 無効化伝播(Invalidation Propagation): 変更されたファイルに依存するすべてのシンボル(関数、クラス、トレイト、エイリアス)をグラフ上で逆流し、ダーティ(Dirty)フラグを立てる。
3. 再評価(Re-evaluation): ダーティと判定されたノード群のみを並列で型チェックする。

壊滅的なアンチパターン:密結合と「巨大なファットファイル」

ここで問題になるのが、「依存の扇形拡散(Fan-out)」だ。
例えば、次のようなコードベースを考えてほしい。

// 悪い例: あらゆる定義を詰め込んだグローバルファットファイル
<<__Strict>>
namespace Hack\Extreme\AntiPattern;

type MegaContext = shape(
‘db’ => \PDO,
‘cache’ => \Memcached,
‘logger’ => LoggerInterface,
‘config’ => ImmMap,
// ほか数百のドメイン型がここに定義されている…
);

class GlobalContainer {
// 依存関係がこのクラスに集中し、全ファイルの9割がこのファイルを指す
}

この `GlobalContainer` や `MegaContext` の型定義にわずか1文字の変更を加えた瞬間、型チェッカーは何が起きるか?
全ファイルの9割が「ダーティ」判定を受け、インクリメンタル・チェックが実質的なフルビルド(Cold Start)へと劣化する。
数秒で終わるはずのチェックが数十秒〜数分に跳ね上がり、開発体験は完全に崩壊する。

—

2. 依存関係の断絶:モジュール分割と「単方向依存(Acyclic Graph)」の強制

この現象を防ぐ唯一の解法は、依存グラフの深さと幅を数学的に制御することだ。Hack言語の厳格な名前空間と、モジュール境界を意識した設計がここで生きる。

階層化アーキテクチャの厳守

コードベースを以下の4層に厳格に分離し、下位層が上位層に依存することをコンパイラレベル(または静的解析ツールレベル)で完全に禁止する。

[Layer 3: Application / Entrypoints] (HTTP Controllers, CLI Commands)
↓ (依存可)
[Layer 2: Domain Services] (ビジネスロジック, ユースケース)
↓ (依存可)
[Layer 1: Domain Models & Entities] (純粋なデータ構造, 不変オブジェクト)
↓ (依存可)
[Layer 0: Primitives & Interfaces] (プリミティブな型エイリアス, インターフェース)

この構造により、Layer 0 や Layer 1 の変更が波及する範囲は、上位層のごく一部に限定される。依存グラフの「ファンアウト(Fan-out)」を定数オーダーに抑え込むのだ。

—

3. 実践:インクリメンタル耐性の高いコードデザイン

実際に、HHVM型チェッカーのキャッシュ効率を最大化するコード構造を見ていこう。

改善前:密結合な実装

<<__Strict>>
namespace Hack\Extreme;

// あらゆるコンテキストを内包した巨大な処理クラス
class OrderProcessor {
public function process(
shape(‘db’ => \PDO, ‘user’ => UserEntity, ‘logger’ => Logger) $ctx,
Order $order,
): void {
// 処理…
}
}

弊害: `Order` や `UserEntity`、さらにロガーの定義が変わるたびに、このクラス全体が再評価される。

改善後:インターフェースの分離と「最小依存型」の定義

<<__Strict>>
namespace Hack\Extreme\Domain;

/

  • Layer 0: 変更頻度が極めて低いプリミティブ・インターフェース

/
interface ILoggable {
public function getLogContext(): ImmMap;
}

<<__Strict>>
namespace Hack\Extreme\Service;
use namespace Hack\Extreme\Domain;

/

  • Layer 1: 依存を最小限に絞ったサービスクラス
  • 具象的なDBやロガーではなく、最小限のインターフェースにしか依存しない。

/
final class MinimalOrderProcessor {
// 依存範囲を絞ることで、hh_serverのキャッシュ無効化の範囲を最小化する
public function __construct(
private Domain\ILoggable $loggerAdapter,
) {}

public function process(string $orderId): void {
// 依存関係が局所化されているため、型チェッカーのグラフ走査が瞬時に終わる
}
}

—

4. HHVMランタイムと型チェッカーをハックする実用的Tips

さらに大規模なプロジェクト(1,000ファイル超)で `hh_server` のメモリ消費と速度を極限まで最適化するための知見を共有する。

1. `hhconfig` による除外設定の最適化

プロジェクトルートの `.hhconfig` において、テストファイルや自動生成されるコードのディレクトリを適切に `ignored_paths` に指定すること。型チェッカーが不要なASTをメモリ上に構築するコストを排除する。

.hhconfig の極限チューニング例
ignored_paths = [
“^vendor/.”,
“^var/cache/.”,
“^tests/fixtures/generated/.”
]

リソース制限の厳格化
enable_experimental_features = []

2. ダイナミック型(`mixed` / `dynamic`)の排除と「型推論の暴走」の阻止

Strict Mode下であっても、曖昧な型(`mixed` からの安易なキャストなど)を使用すると、型チェッカーは依存関係の解決において全ノードの型情報を総当たりで再計算しようとするケースがある。
常に具象的な型、またはジェネリクス(Generics)を活用し、チェッカーの計算量を $O(1)$ または $O(\log N)$ に落とし込むこと。

// 良い例: 厳格なジェネリクスによるスコープの限定
<<__Strict>>
final class Repository {
public function find(int $id): ?T {
// …
}
}

—

5. チーフアーキテクトからの提言

Hackの厳格な静的型システムは、甘美な毒だ。記述性を高めようとしてグローバルな型定義や巨大なデータ構造(Mega-Shapes)を安易に作れば、それはそのままHHVM型チェッカーのインクリメンタル性能を殺す毒薬へと変わる。

コードを書くとき、常に自問してほしい:
「今書いたこの1行の変更は、HHVMの依存グラフにおいて、何個のファイルを再評価させることになるか?」

この問いを脳内に常駐させられた者だけが、真にスケーラブルなHackアーキテクチャを掌握できる。コードベースの美しさは、そのままコンパイルの速度と直結しているのだ。

タイトルとURLをコピーしました