こんにちは、osyoyuです。RubyKaigi 2025に行ってきました。
Day 2 Keynote "Performance Bugs and Low-Level Ruby Observability APIs" はプロファイラおたくの自分にとって実に心躍るセッションでした。地上最強のRubyプロファイラである ddtrace
(Datadog) を作っているIvoが話してくれる!!!! Keynote Speakerが公開された瞬間の高まりをよく覚えています。当日ももちろんド最前で見てました。
余談ですが、私の作っているプロファイラ “Pf2” も発表中でたびたび触れられて超うれしくなっていました。良すぎる。
ということで、人のフンドシで相撲を取るようで少々恐縮ですが、本稿ではひとりのプロファイラおたく & ddtraceファンとして、Ivo Keynoteについてレポートし、いや〜 すごい! すごすぎる!! という話をしてみようと思います。
Ivo Anjo氏
IvoはDatadogでRuby Continuous Profilerを作っている方です。Datadogの魔法のようなUIの裏でデータを収集している ddtrace
gem (DataDog/dd-trace-rb) を作っている人、と考えてもらうとわかりやすいかもしれません1。RubyKaigi 2024でもプロファイラのトークをされていました(Optimizing Ruby: Building an Always-On Production Profiler)。めちゃくちゃクールなスーパーハッカーです。
発表を追う
そんなIvoのKeynoteを簡単になぞっていこうと思います。現場で見ていた人は下までスキップしてもらっても良いかもしれません。
この発表は、全体を通してCRubyが備えている「観測 (observability)」のためのAPI群の話でした。Rubyプログラムを動作させるだけならば必要にならない、いわば「のぞき穴」とも言えるAPI群ゆえ、身近でない人も多かったかもしれません。しかし、パフォーマンスの問題を解明するプロファイラやAPMはこれらのAPIを組み合わせることで作られているのです。組み合わせが違えばまったく違うツールになるのは大変におもしろいことです。
発表に登場したAPIたち
発表では、 #include <ruby/debug.h>
および #include <ruby/thread.h>
でアクセスできる (1) TracePoint APIs (2) Postponed Job API (3) Frame-profiling API (4) Debug inspector APIs (5) GVL Instrumentation API の5点でした。このAPI群はCインタフェースしか備えておらず、Rubyコードから直接呼び出すことはできません。
それでは少しイメージしづらいだろうということで、これらをラップした新作gem “lowlevel-tookit” をショウケースとしつつ、話が展開されます。ちょっと普段のコーディングからは縁遠く、利用例も充実していないこれらのAPIの “使い方” がコンパクトにまとまっているわけです。そして最後の数分ではこれらを組み合わせて新たなプロファイラを作ってしまう、見事な業を見せてこのKeynoteは締められました。
TracePoint APIs
初手はTracePointです。聞き馴染みのある方もいるかもしれませんが、Rubyから呼び出せる TracePoint
のことではなく、C APIの rb_tracepoint_*()
のことでした。
C版のTracePointはRuby版よりもフックできるイベントが多く、実行中のスレッドが切り替わったとき、新しいオブジェクトが作成されたとき、GCが開始されたとき・markフェーズが終わったとき・sweepフェーズが終わったとき、など各種のinternalなイベントにアクセスすることができます。
オブジェクトの作りすぎのような問題を観察するためには RUBY_INTERNAL_EVENT_NEWOBJ
は不可欠、と話されていました。オブジェクトの生成はそれ自体が重くCPU負荷が大きいのみならず、GCの頻度を上げてしまうことにもつながります。
余談として、 rb_add_event_hook2()
についても語られました。これはTracePoint (C) よりも古くから存在するAPIで、TracePoint (C) からも利用されているそうです(知らなかった)。
Postponed Job API
コールバックを “Safepoint” で呼び出すよう登録できるAPIです。Rubyには “Safepoint” という用語はないですが、「Rubyコードを安全に呼び出せるタイミング」という意味合いで使われていました。
このKeynoteで触れられたObservability APIsの多くはコールバックを取る形をしていますが、そのコールバック中ではRubyコードを呼べるとはまったく限らないのです。これはつまり、Observability APIsから得られた結果をRubyコードから見える形で記録できない、ということでもあります。なにせArrayやHashを作成したり編集したりできないのですから。
そこで、Rubyコードの実行が落ち着き、必要な作業が可能になるタイミングまでフックの実行を「保留」するために使えるのがPostponed Job APIです。便利ですね。ただし、安全さと引き換えに、Observability APIsで得られる結果が部分的に不正確になることがあります(safepoint bias)。
Frame-profiling APIs
rb_profile_frames()
とその仲間たちのAPI群です。Rubyレベルでいうところの Thread#backtrace_locations
に近い挙動を示し、呼び出した瞬間にRubyレベルのスタックトレースを取得することができます。
多くのプロファイラが使っているAPIとのことです。ddtraceはもちろん、Stackprof、Vernier、Pf2、みんなこれに依存しています。
rb_profile_frames()
は「任意の瞬間をキャプチャする」ことが存在意義のAPIですから、Cレベル(Rubyの外)でタイマーを設定し、Rubyコードの実行に「割り込んで」シグナルハンドラ中から呼び出すのが主流の使われかたになっています。シグナルハンドラで実行できるコードには非常に強い制約がありますが、rb_profile_frames()
は実用上問題ないように実装されていて、async-signal-safeと言えるそうです。
Debug inspector APIs
ちょっと趣向を変えて、今度はデバッガーを作るためのAPI群 rb_debug_inspector_*()
です。
これもまた Frame-profiling APIs 同様にバックトレースを取得できるAPIですが、取れる情報の詳細度が高く、その分オーバーヘッドも大きいそうです。プロファイラのためではなく、名前通りデバッガのためのAPIですね。 メソッドの引数に渡されたオブジェクトや、呼び出し元のbindingにアクセスしたり、それらを破壊的に変更することもできます。言われてみればデバッガを作るためには処理系の支援は不可欠ですね。
GVL Instrumentation API
最後はRuby 3.2から登場したGVL Instrumentation API (ruby_internal_thread_add_event_hook()
) です。
RubyのThreadにはGVLを獲得している・獲得していないに加えて、「獲得待ち」の状態があります。この状態はCPUの利用もI/Oもしておらず、単に待っているだけですから、特にマルチスレッドなWebアプリケーションでは無駄であり、削りたいもの、とのことでした。
lowlevel-toolkitのtrack_wants_gvlを使い、「あるスレッドが何秒をGVL獲得待ちで過ごしているか」を簡易に取得する方法がデモンストレーションされました。
プロファイラ3分クッキング
Keynoteの最後では、”Release GVL profiler” をサッと作って見せる技が披露されました。GVLにはリリースされるタイミングが2つあり、片方は100 msが経過したとき2、そしてもう片方はI/Oを開始したときです。
実装はここにありそうです。107行しかなく、非常にコンパクトなので一読するとおもしろいです。
rb_internal_thread_event_hook(on_thread_event, RUBY_INTERNAL_THREAD_EVENT_SUSPENDED | RUBY_INTERNAL_THREAD_EVENT_RESUMED)
でGVLがスイッチしたときに呼ばれるコールバック on_thread_event
を登録し、コールバック内でイベントの情報をRubyのHashに記録する、という形をとっているようです。
ビジュアライザの準備
プロファイラを作ろうと思うと、大変なのがビジュアライザを作るパートです。今回はSpeedscopeにデータを流し込む形を選んだようでした。ビジュアライザの選択の幅は広く、Firefox Profiler, Perfettoにデータを流すのも “surprisingly easy” ということでした。
発表中に登場したAPIを組み合わせることで見事なプロファイラが完成した、というわけです。
ここから下はレポートではなく、私見です。
なにがスゴイのか? (私見)
Rubyはスゴイ
自分の知る限り、VM型の言語でRubyほど充実した観測用API群を提供している処理系はそう多くありません(JVMぐらいでしょうか)。たとえば、Python界ではFrame-profiling APIに相当するものがないゆえ、外部プロセスから python
プロセスのメモリを特権でまるごと読む方式でプロファイリングするpy-spyが広く使われていると聞きます。
「観測」のためのAPIは、処理系の実装詳細を暴き出すことが本質です。Rubyプログラマーは本来的にはガーベジコレクション(GC)の存在すら知る必要がないはずですが、パフォーマンスを追求するときはその挙動について強く意識せざるを得ません。そこを分析するときに役に立つのが今回の発表中で登場したAPI群、というわけです。
私はCRubyのメンテナンス方針に詳しいわけではないですが、実装詳細を大公開するAPIはメンテしたくない、と思うのが開発者の心情でしょう。一度作ってしまったAPIはその振る舞いを容易に変更できないことが知られていますから、たとえばGVLの挙動をつまびらかにするAPIはそうやすやすと作りたくない、というのは想像できます。
RubyユーザーとRuby処理系の開発者の間で良いバランス感覚をもち、議論をすすめているIvoとCRubyの開発者とには頭が上がらない思いでいっぱいです。
ddtraceは機能がスゴイし安定性もスゴイ
冒頭に「地上最強のRubyプロファイラである ddtrace
」と書いた理由ですが、これは機能の充実ぶりだけでなく、その安定性にも理由があります。
発表中でも触れられていましたが、CRubyの観測用のAPIは得てして不安定です。たとえばNEWOBJのフックで T_IMEMO
のオブジェクト(完全にRuby internalなデータ構造だが、都合上RubyオブジェクトとしてGCに管理させているもの)を得てRubyコードから読もうと思った瞬間にsegfaultに見舞われることになります。実に共感できる話です。
ところが、Datadogのユーザーから「ddtrace
gem をインストールしたらRailsがクラッシュするようになった」とか「急に遅くなった」のような話は耳にしません。
ddtrace の内部ドキュメントを読むと、「絶対にユーザー環境を壊さない」ことが強く意識されていることが分かります。たとえ必要なAPIがまったくないRuby 2.5であっても、インストールは成功して環境を壊しはしない、というポリシーを設定しているようです。
というわけで、いやー、Ivo Keynote 良かった、と思っているのでした。RubyKaigiの参加者に向けて、CRubyの内部をひらく方法はこんなにたくさんある、ぜひ覗きにきてほしい、そういったメッセージを強く感じているのです。私もやっていきの気持ちが非常に高まるキーノートでした。
スマートバンクからのRubyKaigiレポートは実は他にも2本あります。合わせてごらんください。
RubyKaigi 2025 セッションレポート(前編) #rubykaigi - inSmartBank RubyKaigi 2025 セッションレポート(後編) #rubykaigi - inSmartBank
スマートバンクではRubyKaigiのキーノートに興味のあるエンジニアを募集しています。
- Ruby以外の言語用のSDKとの共通部分であるlibdatadog (Rust製) にもコミットしていることも窺えます。まさにスーパーハッカー。↩
-
デフォルト。実はRuby 3.4では環境変数
RUBY_THREAD_TIMESLICE
で変更できるようになっています https://bugs.ruby-lang.org/issues/20861↩