Read OSS

main.ts から最初の描画まで:VS Code のマルチプロセス起動シーケンス

上級

前提知識

  • 記事 1:アーキテクチャとレイヤリング
  • Electron のマルチプロセスモデル(メインプロセスとレンダラープロセス)の基本的な理解

main.ts から最初の描画まで:VS Code のマルチプロセス起動シーケンス

VS Code を起動するたびに、少なくとも 3 つの OS プロセスをまたぐ精巧な起動シーケンスが走り、数百のサービスが初期化されます。それでも、モダンなハードウェア上ならエディターは 1 秒足らずで表示されます。この記事では、src/main.ts の最初の行からレンダリングされた Workbench シェルまでを順に追い、正確さと起動速度のバランスの取り方を解説します。

Electron メインプロセスの起動

すべては src/main.ts から始まります。ここは Electron メインプロセスのエントリーポイントです。Electron の app.ready イベントが発火するより前に、このファイルは重要な同期処理を実行します。

  1. パフォーマンスマークperf.mark('code/didStartMain') で即座に計測を開始する。
  2. ポータブルモードconfigurePortable(product) でポータブルインストールかどうかを確認する。
  3. CLI 引数のパースparseCLIArgs() が minimist を使って早期にフラグを検出する。
  4. サンドボックスの設定 — 明示的に無効化されていない限り、Chromium のプロセスサンドボックスを有効にする。
  5. ユーザーデータパスgetUserDataPath()app.ready より前にプロファイルディレクトリを解決する。
  6. プロトコル登録vscode-webview://vscode-file:// のカスタムスキームを適切なセキュリティ権限とともに登録する。
  7. クラッシュレポーター — 起動中のクラッシュも確実に捕捉できるよう、同期的に構成する。
sequenceDiagram
    participant OS as Operating System
    participant Main as src/main.ts
    participant Electron as Electron Runtime
    participant CodeMain as CodeMain

    OS->>Main: Launch process
    Main->>Main: Parse CLI args
    Main->>Main: Configure sandbox
    Main->>Main: Set userData path
    Main->>Main: Register protocols
    Main->>Main: Configure crash reporter
    Main->>Electron: Register app.ready listener
    Electron->>Main: app.ready event
    Main->>Main: Resolve NLS configuration
    Main->>Main: bootstrapESM()
    Main->>CodeMain: import('./vs/code/electron-main/main.js')

ここで重要なのは、src/main.tsapp.ready を待つ前にできる限り多くの同期処理を済ませているという点です。Electron はこのイベントをブロックするためです。app.ready が発火すると、onReady() 関数は NLS(ローカライズ)の設定解決とコードキャッシュディレクトリの作成を並列で実行し、続いて startup() を呼び出します。ここで ESM がブートストラップされ、実際のエントリーポイントである src/vs/code/electron-main/main.ts が動的にインポートされます。

CodeMain:シングルインスタンスロックと初期サービス

メインプロセスが本格的に動き始めるのは CodeMain からです。その startup() メソッドは、明確な順序で処理を進めます。

src/vs/code/electron-main/main.ts#L97-L160

ステップ 1:初期サービスの生成。 createServices()(162 行目)がブートストラップ用のサービスコレクションを構築します。対象は Product、Environment、Logger、Log、Files、State、UserDataProfiles、Policy、Configuration、Lifecycle など、自分がプライマリインスタンスかどうかを判断するために最低限必要なサービス群です。

ステップ 2:サービスの初期化。 ディレクトリが作成(mkdir -p 相当)され、状態が読み込まれ、設定がディスクから読み取られます。

ステップ 3:シングルインスタンスロックの取得。 claimInstance() が名前付きパイプ上で IPC サーバーの起動を試みます。成功すれば自分がプライマリインスタンスです。EADDRINUSE で失敗した場合は別のインスタンスがすでに起動しており、クライアントとして接続して CLI 引数を転送した後、自身は終了します。

flowchart TD
    A["CodeMain.startup()"] --> B["createServices()<br/>Bootstrap ~15 services"]
    B --> C["initServices()<br/>mkdir, load state, init config"]
    C --> D["claimInstance()<br/>Try IPC server bind"]
    D -->|Success: first instance| E["Create CodeApplication"]
    D -->|EADDRINUSE| F["Connect as IPC client"]
    F --> G["Forward args to primary instance"]
    G --> H["Exit"]
    E --> I["CodeApplication.startup()"]

このシングルインスタンスのパターンは不可欠です。これがなければ、2 つの VS Code インスタンスがステートファイル、extension host、ユーザーデータを奪い合うことになります。IPC 転送の仕組みにより、ターミナルからファイルを開いても、常に既存のウィンドウにルーティングされます。

ヒント: VS Code が起動せず "Another instance is running" というエラーが表示される場合、古い IPC パイプが残っている可能性があります。Linux では $XDG_RUNTIME_DIR、macOS では ~/Library/Application Support/ に存在するソケットファイルを削除すると解決できます。

CodeApplication:メインプロセスのシングルトン

インスタンスロックを取得したら、次は CodeApplication が処理を引き継ぎます。これはメインプロセスのシングルトンで、約 12 のサービスが DI によって注入されて生成されます。

src/vs/code/electron-main/app.ts#L159-L172

コンストラクターは即座に Electron セッションのセキュリティ設定(CSP、パーミッションハンドラー、証明書検証)を行い、IPC リスナーを登録します。続いて startup() がメインプロセスの完全なサービスグラフを構築します。

src/vs/code/electron-main/app.ts#L552-L604

startup() メソッドの処理内容は次のとおりです。

  1. タスクバーのグループ化のために Win32 アプリユーザーモデル ID を設定する
  2. レンダラー↔メイン通信用の Electron IPC サーバーを作成する
  3. マシンのテレメトリ ID を解決する
  4. Shared Process をセットアップする — ウィンドウ間で共有されるサービス(extension の管理、テレメトリのバッチ処理、検索インデックスなど)をホストする、非表示の Electron レンダラーです
  5. initServices() を呼び出して拡張されたサービスグラフを生成する(テレメトリ、ストレージ、ワークスペース、ウィンドウ管理、アップデートサービス、ターミナル PTY ホストなど)
  6. WindowsMainService 経由でウィンドウを開くか、以前の状態を復元する

レンダラー側のブートストラップ:DesktopMain と Workbench

各 VS Code ウィンドウは Electron レンダラープロセス上で動作します。そのブートストラップは DesktopMain で行われます。

sequenceDiagram
    participant Main as Main Process
    participant Renderer as Renderer Process
    participant DM as DesktopMain
    participant WB as Workbench
    
    Main->>Renderer: Create BrowserWindow
    Renderer->>DM: new DesktopMain(config)
    DM->>DM: reviveUris()
    DM->>DM: initServices() in parallel with DOM ready
    DM->>DM: Create ServiceCollection
    Note over DM: MainProcessService, Product, Environment,<br/>Logger, Policy, SharedProcess, Files,<br/>Remote, Storage, Configuration...
    DM->>WB: new Workbench(document.body, services)
    DM->>WB: workbench.startup()
    WB->>WB: initServices() — collect all registerSingleton() descriptors
    WB->>WB: initLayout()
    WB->>WB: Registry.start() — workbench contributions
    WB->>WB: renderWorkbench() — create UI parts
    WB->>WB: restore() — restore editors, views

desktop.main.ts#L114-L142open() メソッドは、サービスの初期化と DOM の準備を並列で進めます。どちらも I/O バウンドであるため、これは賢い最適化です。また、視覚的なちらつきを防ぐために、Workbench を生成する前にズームレベルを明示的に適用しています。

initServices() の中に目立つコメントがあることに気づくでしょう。「NOTE: Please do NOT register services here. Use registerSingleton() from workbench.common.main.ts」とあります。これは記事 1 で解説したバレルファイルの仕組みが活きているところです。DesktopMain が直接登録するのは、メインプロセスの IPC チャネルへの直接アクセスが必要なサービスだけです。それ以外はすべて、グローバルに登録済みのシングルトンから提供されます。

プロセスの全体像:6 つの役割

VS Code は「メイン + レンダラー」という単純な構成ではありません。6 種類のプロセスが役割を担っています。

graph TD
    MAIN["<b>Main Process</b><br/>Electron main<br/>Window management, OS integration,<br/>lifecycle, IPC hub"]
    
    RENDERER["<b>Renderer Process</b><br/>Electron renderer (per window)<br/>Workbench UI, editor, panels"]
    
    SHARED["<b>Shared Process</b><br/>Hidden Electron renderer<br/>Extension management, search indexing,<br/>telemetry batching"]
    
    UTILITY["<b>Utility Processes</b><br/>Node.js child processes<br/>PTY host (terminal), file watcher"]
    
    EXTHOST["<b>Extension Host</b><br/>Node.js / Web Worker<br/>Runs extension code in isolation"]
    
    SERVER["<b>Remote Server</b><br/>Headless Node.js<br/>SSH/container/WSL backend"]
    
    MAIN --> RENDERER
    MAIN --> SHARED
    MAIN --> UTILITY
    RENDERER --> EXTHOST
    SERVER --> EXTHOST
    
    style MAIN fill:#e3f2fd
    style RENDERER fill:#e8f5e9
    style SHARED fill:#fff3e0
    style UTILITY fill:#f3e5f5
    style EXTHOST fill:#fce4ec
    style SERVER fill:#e0f2f1
  • メインプロセス — Electron の「頭脳」です。ウィンドウを管理し、OS レベルの統合(メニュー、Dock、ファイルの関連付け)を担い、他のすべてのプロセス間の IPC を調整します。
  • レンダラープロセス — VS Code ウィンドウごとに 1 つ存在し、Workbench の UI 全体を実行します。サンドボックス化されており、Node.js への直接アクセスはできません(IPC 経由でメインプロセスと通信します)。
  • Shared Process — ウィンドウをまたいで処理をキャッシュ・共有するヘッドレスレンダラーです。extension ギャラリーへのクエリ、テレメトリの集約、設定の同期などを担当します。
  • Utility Processes — CPU/IO 集中型の処理に使う Electron の UtilityProcess API です。代表例は PTY ホスト(ターミナルのバックエンド)で、すべての疑似端末インスタンスを管理します。
  • Extension Host — サードパーティの extension コードを実行します。レンダラーから分離されており、ローカルの Node.js プロセス、Web Worker、または別マシン上のリモートプロセスとして動作します。
  • Remote Server — リモート開発(SSH、コンテナ、WSL)のためのヘッドレスバックエンドです。リモートマシン上で extension host とファイルシステムアクセスをホストします。

CLI とリモートサーバー:別の起動経路

VS Code の起動は必ずしも Electron を経由するわけではありません。src/cli.ts の CLI エントリーポイントは、わずか 26 行の軽量なスクリプトです。NLS を解決し、ESM をブートストラップし、VSCODE_CLI=1 をセットして CLI ハンドラーをインポートします。code --install-extensioncode --diff といった操作に使われます。

src/server-main.ts のリモートサーバーはより本格的です。サーバー固有の CLI 引数をパースし、必要に応じてライセンス同意を求め、HTTP サーバーを作成し、リモート extension host エージェントを遅延ロードします。ここで重要な設計判断があります。HTTP サーバーは VS Code サーバーの完全なモジュールを読み込む前にリッスンを開始します。これにより、ポートはすぐに確保され、最初のリクエストが届いたタイミングでコストのかかる初期化がトリガーされます。

flowchart LR
    A["server-main.ts"] --> B["Parse args"]
    B --> C["Create HTTP server"]
    C --> D["server.listen()"]
    D --> E["First request arrives"]
    E --> F["Lazy load server.main.js"]
    F --> G["Create extension host agent"]

この遅延ロードのパターンにより、バックグラウンドで数百のサービスをロード中であっても、サーバーは Remote-SSH extension のような接続ツールに対してすぐに応答できる状態を維持できます。

次回予告

起動中に何が初期化されるかはわかりました。しかし、コンストラクターを手動で呼び出すことなく、数百のサービスがどのようにして接続されるのかについては、まだ触れていません。次の記事では、VS Code 独自の依存性注入システム、つまりこのサービスグラフ構築を支える createDecorator/InstantiationService パターンを深く掘り下げます。