Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

RubyKaigi 2026 - Chasing Real-Time Observability for CRuby (Day2)

ogp

はじめに

ジョブハウスで開発をしている @Kaffff です。

本記事では、RubyKaigi 2026 の2日目に行われた Shintaro Otsuka@White_Green2525)さんのセッションについて紹介します。CRuby の内部動作を3Dでリアルタイムに可視化するツール「rrtrace」についてのセッションになります。

speakerdeck.com

rrtrace とは

rrtrace は、CRuby の内部動作を 時間 × スレッド × スタック深度 の3軸でリアルタイムに3D可視化するツールです。

rrtrace でフィボナッチ関数を観測している様子

セッションではデモとして、まず IRB を立ち上げた状態で rrtrace を起動していました。画面上をメソッド呼び出しの箱が流れていき、キーボード入力を処理するたびにスタックが積み上がります。フィボナッチ関数を実行するとスタックが高く積み上がっていく様子が可視化され、CPU を消費する処理では関数の実行時間が長い分、箱が横に伸びていきます。

Rails の Web サーバーを起動してブラウザからリクエストを送るデモもあり、リクエスト処理のたびにスタックが積み上がる様子をリアルタイムに確認できていました。

アーキテクチャの全体像

rrtrace は CRuby プロセスとは別に Visualizer プロセスを起動する構成になっています。

CRuby プロセス(C拡張)→ [共有メモリ] → Visualizer プロセス(Rust + GPU)

CRuby 内で発行される TracePoint 等のイベントを C 拡張でキャプチャし、共有メモリに書き込みます。Visualizer プロセスはそこからイベントを読み取り、Rust + GPU で描画します。方針として「C拡張の中では複雑な処理を行わず、CRuby の実行の邪魔をしない」ことが重視されていました。

CRuby 側で取得するイベントは主に3種類です。

  • TracePoint API による CALL / RETURN
  • INTERNAL_GC_ENTER / INTERNAL_GC_EXIT(GC の開始・終了)
  • INTERNAL_THREAD_EVENT(スレッドの STARTED / READY / SUSPENDED / RESUMED / EXITED)

例えば add(1, 2) のようなメソッド呼び出しなら、CALL イベントと RETURN イベントが発行されます。内部で Integer#+ を呼び出していればその分のイベントも追加で発行されます。スレッドの場合は、生成時に STARTED が発行され、GVL による実行状態の切り替わりで SUSPENDED / RESUMED が呼ばれ、終了時に EXITED が発行されます。

取得したイベントは、種類・タイムスタンプ・メソッドIDやスレッドIDを組み合わせて、1イベントあたり16バイト(128ビット)の統一フォーマットにまとめられます。すべてのイベントを同じサイズに揃えることで後続の処理が楽になる狙いがあります。

共有メモリとリングバッファ

CRuby プロセスから Visualizer プロセスへのデータ転送には、OS が提供する共有メモリの仕組みを利用しています。この共有メモリ上にリングバッファを構築し、通信経路として使っています。

排他制御にはスピンロックを採用しており、ロック待ちの際にコンテキストスイッチを介さないので、最低限のオーバーヘッドでイベントを送信できます。

rrtrace ではイベントを漏らさず処理することが重視されています。Visualizer 側の処理が遅れてバッファが満杯になった場合、CRuby 側は送信できるまで待つようになっており、イベントが捨てられることは基本的にありません。

裏を返せば、Visualizer の処理が遅れるほど CRuby 側が待たされます。そこで受信側では、共有メモリから複数のイベントをまとめて一括で受け取り、内部の処理用キューに逃がしています。リングバッファのスペースを素早く空けることを最優先にすることで、CRuby 側が止まってしまう事態を防いでいます。

スタックのシミュレーションと可視化

Visualizer 側では、受け取ったイベントを順番に走査して、スレッドごとにコールスタックの状態をシミュレートしていきます。

stack = []
events.each do |event|
  case event
  in [:call, timestamp, method_id]
    stack << [timestamp, method_id]
  in [:return, end_timestamp, _method_id]
    start_timestamp, method_id = stack.pop
    # x: start..end, y: thread_id, z: stack.size, color: method_id
  end
end

CALL で push、RETURN で pop。pop した時点でメソッドの「呼び出し開始時刻」がわかり、RETURN イベントのタイムスタンプが「終了時刻」になります。開始時刻・終了時刻・スレッドID・スタック深度が揃えば、3D空間上の直方体として描画できます。色はメソッドIDから決めています。

リアルタイム処理のための並列化

リアルタイムで可視化するには、CRuby のメソッド呼び出し速度と同等以上の速さでスタックシミュレーションを処理しなければなりません。処理が遅いと CRuby 側はどんどん先へ進むのに描画が追いつかず、内部キューにイベントが溜まり続けてメモリ使用量が際限なく増えてしまいます。

そこで、並列処理です。CRuby は GVL により基本1コアしか使われないため、開発マシンの残りのコアをシミュレーションに回すことができます。ところが、スタックのシミュレーションは直前のイベントの結果に依存するので、単純にはいきません。CALL 1, CALL 2, RETURN 2(1と2はメソッドID)というイベント列で考えてみます。順番通りなら1と2を push して2を pop します。しかし、1を push せずに2を push したり、何も push していない状態で pop しようとすると全く違う結果になります。push されていない情報は pop できません。

セッションでは、これを Parallel scan アルゴリズムの応用で解決したと紹介されていました。大量のイベントをいくつかのブロックに分割し、ブロックごとに「pop できなかったイベントは何か」「最終的にスタックに何が残っているか」を集計します。例えば CALL 1, CALL 2, RETURN 2 というブロックなら、最終的にスタックに残るのは「1」だけ。別のブロック RETURN 1, CALL 3, CALL 4 なら、最初の時点で1は pop できないので「1を pop できなかった」という情報を記録し、3と4を積みます。この集計は他のブロックの結果に依存しないので並列化できます。

集計が終わったら、結果を前から順番にマージしていきます。1個目のブロックで残った「1」と、2個目のブロックの「1を pop できなかった」を相殺すると「3と4」が残るとわかります。このマージ処理は直列ですが、ブロックあたりのイベント数を1000個や1万個と大きくすれば、直列部分の割合は十分小さくなります。確定した各ブロックの開始状態から正確なスタック状態を計算する処理もブロック単位で独立しているので並列化可能です。こうして、開発マシンの余ったコアをフルに使い切ってリアルタイム描画を実現しています。

パフォーマンスへの影響

セッションではベンチマーク結果も示されていました。2種類のベンチマークが用意されています。ひとつは空の関数をひたすら呼び出して1秒間の呼び出し回数を計測するマイクロベンチマーク。もうひとつは Rails の Web サーバーに ab コマンドでリクエストを送り、RPS を計測したものです。

「empty TracePoint handler」は TracePoint 自体のオーバーヘッドを切り分けるために、中身が空の関数で登録した場合の数値です。「rrtrace without sending event」はイベントの収集だけ行い Visualizer への送信はしない状態で、ボトルネックの切り分け用に計測したとのことでした。

上から順に見ると、TracePoint 自体のコスト、タイムスタンプ取得のコスト、リングバッファでの送信コストを分離できます。TracePoint の処理自体がかなり重い一方で、リングバッファを使った送信処理の最適化はしっかり効いています。

ただ、Rails のベンチマークではタイムスタンプ取得のコストが少し目立ってきています。推測では、マルチスレッド環境で複数スレッドから同時に送信処理が走ることで、排他制御の過程でキャッシュミスが多発しているのではないかとのことでした。

今後の課題

パフォーマンスにはまだ改善の余地があること、マルチプロセスや Ractor への対応が未完了であること。現状のシステムは GVL を前提に設計されているため、対応するにはアーキテクチャから考え直す必要があるそうです。

また、現在の画面ではメソッドIDから適当に色をつけているだけで、「今実行されている長い処理が何か」「対応するソースコードの何行目か」といった情報はまだ表示できません。一時停止や巻き戻し機能も実現したいとのことですが、メモリ増大を防ぐために描画済みのイベントデータを順次捨てている現状の設計とは相性が悪く、難しい課題として残っているようです。

感想

デモがとにかくすごかったです。3D空間をイベントが流れていく様は音ゲーみたいで、見ていて非常に面白かったです。プログラムの実行にスタックが用いられることは理論的には理解していましたが、実際に可視化された結果を見ると「本当にスタックが動いているんだな」という実感が湧き、なんとも言えない感動がありました。

また、パフォーマンスチューニングのための設計も非常に面白く、勉強になりました。リングバッファや並列処理を組み合わせて関数呼び出しをリアルタイムにトレース・レンダリングしており、実際に手元の M1 MacBook Pro で動かしてみたのですが、強力な GPU を積んでいなくても問題なく動作することに驚きました。

この発表を通じて、低レイヤの最適化や並列処理への興味がより一層深まりました。素晴らしい発表をしてくださった Otsuka さん、本当にありがとうございました。

Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com