live-server の配信アーキテクチャ
端末の操作画面を Web ブラウザへリアルタイムに中継するには、ミリ秒単位の低遅延性と、大量のテキスト描画に耐えうる描画効率の両立が求められます。
console2svg の live-server サブコマンドは、外部の Web サーバーや Node.js ランタイムに依存せず、.NET の標準ソケット(TcpListener)を用いて軽量な HTTP/1.1 サーバーと SSE(Server-Sent Events:HTTP 上でサーバーからクライアントへ一方向のテキストイベントをストリーミング配信する規格)を自前で実装しています。
不要なプロトコルオーバーヘッドを削ぎ落とし、単一の静的バイナリだけで快適なブラウザ配信を実現しています。
組み込み HTTP サーバーのエンドポイント設計
Section titled “組み込み HTTP サーバーのエンドポイント設計”live-server は既定で 127.0.0.1:38473 でリッスンを開始します。
ブラウザからの要求に対して、以下の 5 つの最小限のエンドポイントを提供します。
/: クライアント側のビューア画面(HTML / CSS / JavaScript を 1 つにまとめた埋め込みリソース)を返却します。/events:text/event-stream形式でターミナルの最新描画データを連続送信する SSE ストリームです。/snapshot.svg: 現在の画面状態を完全な単一 SVG 画像としてオンデマンドで生成して返却します。/health: サーバーが正常稼働しているかを確認するためのヘルスチェック用エンドポイント({"status":"ok"})です。/settings: 画面フィットモードやデバッグ表示を切り替える設定画面です。
WebSocket ではなく SSE を採用している理由は、双方向通信が不要な画面配信においてハンドシェイク処理やパケットヘッダーを最もシンプルに保てるためです。
また、プロキシ環境や標準的な HTTP キャッシュとの親和性が高く、ブラウザ標準の EventSource API だけで自動再接続やイベント分岐を扱える利点があります。
三層レイヤー分離による転送量と再描画の削減
Section titled “三層レイヤー分離による転送量と再描画の削減”画面が更新されるたびに、ウィンドウ枠や影を含む完全な SVG 全体を送信していては、ネットワーク帯域を過剰に消費し、ブラウザ側の DOM(Document Object Model)再構築負荷が高まります。 この無駄を排除するため、console2svg は SVG の描画内容を 三層の独立したレイヤー に分離して配信します。
- 背景レイヤー(
background): 端末ウィンドウの外側余白や、クライアント領域全体のベース背景矩形を描画します。画面サイズが変わらない限り内容は変化しません。 - ウィンドウレイヤー(
window): ウィンドウ枠、タイトルバー、ドロップシャドウ、コントロールボタンを描画します。サイズ変更やタイトル文字列の変更時のみ更新されます。 - テキストレイヤー(
text): ターミナル内部の文字、文字装飾、カーソル、行ごとの背景色を描画します。コマンドの出力に応じて高頻度で更新されます。
event: backgrounddata: <svg xmlns="http://www.w3.org/2000/svg" width="800" height="500">...</svg>
event: windowdata: <svg xmlns="http://www.w3.org/2000/svg" width="800" height="500">...</svg>
event: textdata: <svg xmlns="http://www.w3.org/2000/svg" width="800" height="500">...</svg>ブラウザ側では、重なり合う 3 つのコンテナ要素(#background、#window-paint、#text)を用意し、受信したイベント名に応じて該当レイヤーの innerHTML だけを差し替えます。
通常のコマンド実行中やテキスト編集時には最前面の text レイヤーのみが更新されるため、静的なウィンドウ装飾の再パースを完全にスキップできます。
サーバー側の描画ループと Settle Delay
Section titled “サーバー側の描画ループと Settle Delay”端末エミュレーター(ScreenBuffer)の内容が書き換わった瞬間、即座に SVG を生成して配信するわけではありません。
多くの CLI ツールは 1 画面分の更新を細かなエスケープシーケンスに分割して書き込むため、即時描画を行うと画面が中途半端に消去された状態やカーソルが移動中の瞬間を配信してしまうためです。
サーバー側の描画ループ(RenderLiveFramesAsync)は、内部タイマー(約 60fps 周期)で画面のバージョン番号(screenVersion)を監視し、以下の制御を行います。
- Settle Delay(安定化待機): 画面更新を検知した後、直前の書き込みから 25 ミリ秒間(
LiveFrameSettleDelay)新たな更新が発生しないことを確認してから描画に入ります。連続した出力を 1 フレームにまとめることで、描画のチラつきを抑えます。 - FPS 上限スロットリング: 連続した大量のログ出力が続く場合でも、最短フレーム間隔(
--video-fps設定に連動、既定 30fps の場合は約 33 ミリ秒)を超えて描画を行いません。CPU 資源の過剰消費を防ぐためです。 - 静的レイヤーのキャッシュ: 画面の列数・行数が直前のフレームと一致している場合、
backgroundレイヤーの SVG 生成をスキップして前回の生成結果を再利用します。
バックプレッシャー制御と遅延蓄積の防止
Section titled “バックプレッシャー制御と遅延蓄積の防止”ネットワークが低速なクライアントや、ブラウザのタブがバックグラウンドに回ったクライアントが存在する場合、送信キューにフレームが溜まって配信遅延が際限なく増大するリスクがあります。 リアルタイムプレビューにおいて、数十秒前の古い画面が遅れて再生される動作は致命的です。
各クライアントを管理する LiveSseClient は、バックプレッシャー(受信側の処理能力を超えたデータ滞留)を回避するため、未送信キューの上書きマージ処理を備えています。
クライアントへのソケット書き込みが完了していない間に新しいフレームが生成された場合、未送信の古いフレームを破棄し、最新の SVG データでバッファを上書きします。
この最新化処理により、ネットワークの帯域が狭い環境であっても、クライアントには常に「最新の端末画面」だけが届きます。
また、ソケット書き込みには 5 秒のタイムアウトを設けており、応答が途絶えたクライアントは自動的に切断され、サーバー側のメモリリークを防ぎます。
さらに、通信が途切れていないことを証明するため、2 秒周期で空の heartbeat イベント(ping)を配信します。
ブラウザ側の rAF コアレッシングと性能監視
Section titled “ブラウザ側の rAF コアレッシングと性能監視”サーバー側で間引きを行っていても、高速スクロール時などには 1 秒間に数十回の text イベントがブラウザへ届くことがあります。
ブラウザがイベントを受信するたびに同期的な innerHTML の代入を行うと、レンダリングエンジンのパース処理とスタイル再計算が追いつかず、UI がフリーズしてしまいます。
ビューアの JavaScript は、この問題を防ぐために rAF コアレッシング(Burst Coalescing)を実装しています。
text イベントを受信した際はデータを一旦メモリ上の変数(pendingText)へ保持するだけに留め、実際の DOM 反映は requestAnimationFrame のタイミングに同期して実行します。
直前のフレーム描画が完了するまでの間に複数のイベントが到着した場合、途中のフレームは自動的に読み飛ばされ、次の画面描画サイクルで最新の 1 フレームだけが反映されます。
また、ビューアにはリアルタイムなパフォーマンス診断パネルが組み込まれています。
- イベントレート(events/s): 1 秒間に適用されたフレーム数を青色ラインでプロットします。
- Jank メーター(jank ms/s): 画面のリフレッシュレート(垂直同期)から算出される期待フレーム間隔を超過した遅延時間を赤色ラインでプロットします。描画が完全に滑らかな状態では 0 を示します。
- LoAF(Long Animation Frames)監視: ブラウザの Long Animation Frames API を用いて、50 ミリ秒以上メインスレッドを占有した描画ブロックを検出し、その所要時間と原因をログへ記録します。
さらに、ブラウザのウィンドウサイズに応じた 4 種類の表示モード(画面内に最大化して収める contain、幅基準の width、高さ基準の height、端末ピクセル等倍の actual)をサポートし、どのような画面解像度でも鮮明な表示を維持します。