Skip to content

Support profiling with vernier - #79

Merged
dduugg merged 2 commits into
mainfrom
add-vernier-profiler
Sep 29, 2026
Merged

dduugg merged 2 commits into
mainfrom
add-vernier-profiler

Conversation

@dduugg

@dduugg dduugg commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #26.

Vernier profiles each thread separately, so a flamegraph from a multi-threaded app like Puma or Sidekiq isn't a mix of what all its threads were doing. This adds it as a second profiler alongside stackprof, which stays the default.

Usage

# Gemfile
gem "vernier"

# One flamegraph. Singed.start and controllers' `flamegraph` take `profiler:` too.
flamegraph(profiler: :vernier) { ... }

# Or make it the default, including for the RSpec, controller, Rack and Sidekiq integrations
Singed.profiler = :vernier

How it works

  • Vernier is optional. It's a native extension that only some apps want, so it isn't in the gemspec. Singed requires it when asked for it. If the gem is missing, it raises a LoadError saying to add it. If vernier is installed but fails to load, such as a native extension built for another Ruby, the original error comes through unchanged. It also raises if the loaded vernier is older than 1.5, since the gemspec can't pin an optional gem's version. Singed.profiler = does all of this too, so a missing or outdated gem, or a typo, fails at boot rather than on the first profiled request. The README says to set it only where vernier is bundled.
  • Flamegraphs still open in speedscope. Speedscope can't read Vernier's default Firefox Profiler format, and Vernier's cpuprofile output only includes the main thread. So the new Singed::VernierReport converts Vernier's result into speedscope's file format itself:
    • One profile per thread. Speedscope opens on the thread that ran the profiled code, and you switch threads from its title bar or with n/p. Threads that recorded no samples are left out.
    • Vernier keeps sampling threads that are waiting. Those samples get an (idle) or (waiting for GVL) frame on top, so time spent waiting doesn't look like time spent running the code that started waiting.
    • One frame per method rather than per line, so each method is a single box in the flamegraph, as with stackprof. File paths go through Singed.filter_line, the same as stackprof and rbspy output. Function, file and thread names are made valid UTF-8 first. Vernier doesn't check its strings, and it names unnamed threads after Thread#inspect, which is binary; either kind would otherwise make JSON.generate raise or warn.
  • Each flamegraph gets its own Vernier::Collector, rather than using Vernier.start_profile, which raises if a profile is already running.
  • ignore_gc does nothing with Vernier, because Vernier doesn't sample a thread while it's running GC. The README says so.
  • flamegraph.profile is the raw Vernier::Result, as it's the raw StackProf.results for stackprof. That means the same recording can also be saved for vernier.prof, which shows GVL and GC activity.
  • Signatures don't name any Vernier::* constant. Tapioca evaluates them in apps that may not load vernier, so profile and stop become untyped.

#29 tried this before, opening Vernier's Firefox-format output with profile-viewer. This PR keeps speedscope as the one viewer so that everything Singed already does, including the bundled offline speedscope, works unchanged.

Not in this PR

  • The singed command line still records with rbspy.
  • Speedscope doesn't show Vernier's GC and GVL markers. Use the raw result with vernier.prof for those.

Testing

  • bundle exec rspec passes on Ruby 3.3.11, 3.4.11 and 4.0.5, with vernier 1.11.0 compiling on each. srb tc and rubocop are clean.
    • VernierReport specs use verifying doubles of Vernier::StackTable and Vernier::Result, so the data is deterministic and the method names are still checked against Vernier.
    • The Flamegraph and Singed.start/stop specs record with real Vernier, including a second thread.
  • Output from a three-thread script passes speedscope's published JSON schema.
  • I rendered that output in the vendored speedscope 1.24.0 using headless Chrome. All three thread profiles loaded, with (idle) under Kernel#sleep and (waiting for GVL) while another thread held the GVL.
  • On about 4.6 s of in-process RuboCop (4,339 samples, 20,906 stacks), save took 0.06 s and wrote 1.6 MB. stackprof wrote 6.5 MB for the same work.
  • Vernier 1.5 is the oldest version with everything this relies on, so the README asks for 1.5 or newer and Singed checks for it. is_start, meta[:interval] and the StackTable accessors date from 1.1, but the public Vernier::Result#stack_table reader arrived in 1.5.
  • The first commit only adds vernier to the development bundle and its generated RBI; the second has the feature.

Singed is about to support profiling with vernier as an optional
dependency, so its specs and Sorbet need the gem. The RBI is generated
with `bin/tapioca gems`.
@dduugg
dduugg requested a review from a team as a code owner September 29, 2026 21:03
@github-project-automation github-project-automation Bot moved this to Triage in Modularity Sep 29, 2026
Closes #26.

Vernier samples each thread separately, so a flamegraph from a
multi-threaded app isn't a mix of what all its threads were doing.
Pass `profiler: :vernier` to `flamegraph`, `Singed.start` or a
controller's `flamegraph`, or set `Singed.profiler = :vernier` to make
it the default for every integration. stackprof stays the default.

Vernier isn't a dependency of the gem: it's a native extension that
only some apps want, so Singed requires it when asked to use it and
explains what's missing if it can't.

Singed::VernierReport converts Vernier's result to speedscope's file
format, with a profile per thread, so flamegraphs still open in the
bundled speedscope. Samples from threads that were waiting get an
`(idle)` or `(waiting for GVL)` frame on top, rather than looking like
time spent in the code that started waiting.
@dduugg
dduugg force-pushed the add-vernier-profiler branch from c00fa65 to dd2d2dc Compare September 29, 2026 21:31
@dduugg
dduugg merged commit 7983cbf into main Sep 29, 2026
15 checks passed
@dduugg
dduugg deleted the add-vernier-profiler branch September 29, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Support vernier

1 participant