CLI tool
jhawthorn/vernier avatar
jhawthorn/vernier

Vernier samples Ruby stacks to show where the time and the memory actually went

๐Ÿ“ next generation CRuby profiler

1,124 stars38 forksRubyMIT

At a glance

What is it?
Vernier is a sampling profiler for Ruby 3.2.1 and newer that tracks multiple threads, GVL activity, GC pauses and idle time, and can also record retained allocations rather than a time series. It writes Firefox profiler compatible JSON, and the last push was on 2026-10-18.
Who is it for?
Vernier suits a Ruby developer who wants to profile a live process rather than a benchmark, especially one running threads, a web server or a job queue, since it samples across threads and can record retained memory rather than only a time series. It is the wrong tool if you want deterministic benchmark numbers rather than a picture of what happened, and it needs Ruby 3.2.1 or newer on a Unix-like system.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A sampling profiler, and what that means for the output

Vernier describes itself as a next-generation sampling profiler for Ruby 3.2.1 and newer, and the README's first line is more specific about what it tracks: multiple threads, GVL activity, GC pauses, idle time, and more. It requires Ruby 3.2.1 or greater and a Unix-like operating system.

The sampling model shapes everything about the output. The profiler takes a snapshot of the stack at an interval rather than instrumenting every call, so the overhead is a function of the interval rather than of how much code runs. The interval option defaults to 500 microseconds, and the middleware default is 200, which is faster sampling than the library default.

Two views are offered, and the README is careful to say how to read them. The flame graph shows proportionally how much time is spent within particular stack frames, with frames grouped together, which means the x-axis and left-to-right order is not meaningful. The stack chart shows the stack at each sample with the x-axis representing time, so it can be read left to right.

That distinction is the practical one. A flame graph answers where the time went and is a good tool for spotting a wide expensive frame. A stack chart answers what happened in what order, which is what you want when the question is why a request was slow, because you can see the sequence rather than the aggregate.

The examples the README links show the range: Sidekiq jobs from Mastodon profiled for time and threaded, Puma web requests from Mastodon in the same two modes, a Rails benchmark for lobste.rs, and require irb profiled for retained memory.

Three ways to profile, from a subprocess to a live server

The CLI is the easiest entry point, and it wraps a command:

sh
$ vernier run -- ruby -e 'sleep 1'
starting profiler with interval 500 and allocation interval 0
#<Vernier::Result 1.001589 seconds, 1 threads, 1 samples, 1 unique>
written to /tmp/profile20240328-82441-gkzffc.vernier.json.gz

The second form sets both intervals, and the sample count in the result line tells you how much data you actually collected, which is a useful sanity check when a profile looks empty:

sh
$ vernier run --interval 100 --allocation-interval 10 -- ruby -e '10.times { Object.new }'

For code inside a process, the block form writes to a file you name:

ruby
Vernier.profile(out: "time_profile.json", interval: 100, allocation_interval: 10) do
  some_slow_method
end

The aliases Vernier.run and Vernier.trace do the same thing, and there is a start and stop pair for the case where the region of interest is not contiguous:

ruby
Vernier.start_profile(out: "time_profile.json", interval: 10_000, allocation_interval: 100_000)

some_slow_method

# some other file

some_other_slow_method

Vernier.stop_profile

Note the interval of 10_000 microseconds there, ten milliseconds, which is the kind of value you use when the profiled region is long and you do not want to drown in samples.

The output is a gzipped JSON file, and it can be read in the web app at vernier.prof, locally with the profile-viewer gem, or from the command line with vernier view. The web app and the profile-viewer gem are both described as lightly customized versions of the Firefox profiler frontend that Vernier profiles are compatible with.

The Rack middleware profiles a running server by query string

The middleware is the feature that turns Vernier from a benchmark tool into a production debugging tool, because it does not require restarting the process with a profiler attached.

For a Rack application you add it to config.ru:

ruby
require "vernier"

use Vernier::Middleware

run ->(env) { [200, { "Content-Type" => "text/plain" }, ["Hello, Profiling World!"]] }

In Rails you add it to config/application.rb, and the README shows the permit option, which is a filter on which requests may be profiled:

ruby
config.middleware.use Vernier::Middleware, permit: ->(env) { env["PATH_INFO"].start_with?("/api") }

Profiling is then turned on per request with query parameters:

sh
curl http://localhost:3000?vernier=true&vernier_interval=100&vernier_allocation_interval=10

That design is the right one for a shared environment. The middleware is always loaded, but it only does work when a request asks for it, and the permit lambda lets you restrict that to an API prefix so that an ordinary user request cannot switch on profiling. Without a permit rule, anyone who can reach the endpoint can generate profiles, and with allocation sampling enabled that is not free.

The option table makes the middleware defaults distinct from the library defaults, and the distinction is worth reading. The sampling interval defaults to 500 microseconds in the library and 200 in the middleware, and the allocation interval defaults to 0, meaning disabled, in the library but 200 in the middleware. So profiling a live server through the middleware collects allocation samples by default, while profiling a block of code does not.

Retained memory is a different question from where time went

The second mode answers a different question, and the README is emphatic that the resulting graph is read differently.

To record all retained allocations from loading a library:

sh
ruby -r vernier -e 'Vernier.trace_retained(out: "irb_profile.json") { require "irb" }'

The note on retained memory flamegraphs says they must be interpreted a little differently than a typical profiling flamegraph. In a retained memory flamegraph, the x-axis represents a proportion of memory in bytes, not time or samples. The topmost boxes on the y-axis represent the retained objects with their stacktrace below, and their width is the percentage of overall retained memory each object occupies.

This is a genuinely different tool. A time profile says the object allocation at this line is hot. A retained profile says this object is still alive at the end of the block, and it is here that the bytes are. For a leak hunt or a memory audit, the second question is the one that matters, and the object that was allocated most often is not necessarily the one holding the most memory.

The mode option controls the choice, with :wall, :retained and :custom available, and :wall is the default. The gc option runs a full GC cycle before profiling and applies only in :retained mode, defaulting to true, which makes sense: you want the retained set to be accurate, and a full collection beforehand removes the garbage that would otherwise be counted ambiguously. The out option names the file, and metadata takes key-value pairs to embed in the profile.

Hooks put application events into the timeline

Hooks add markers to the profile based on application events, and they are how a sampling profile gets tied to what the application was actually doing.

The usage is a list passed to the profile call:

ruby
Vernier.profile(hooks: [:activesupport, MyHook]) do
  # code to profile
end

Two hooks are built in. :activesupport, also available as :rails, adds ActiveSupport notifications as markers, which means a Rails request shows up as a labelled band in the timeline instead of an undifferentiated lump of frames. :memory_usage adds a memory usage counter to the output, so you can see memory moving alongside the CPU timeline rather than in a separate run.

Writing a custom hook means a class with initialize(collector), enable and disable. The collector is what you report into, enable is where you set up event listeners, and disable is where you clean them up. The lifecycle is the important part, because a hook that is not disabled cleanly leaks its listeners into the next run.

For the Firefox profiler integration there are two optional methods. firefox_marker_schema returns a schema describing the marker shape, with a name, a display list such as marker-chart and marker-table, a tooltip label and a data key list. firefox_counters returns a named counter with a category, a description, samples of time, count and length. The README links the Firefox profiler documentation for the format details, and examples/custom_hook.rb has a complete example.

The example files in the repository show the range of things worth profiling: fiber_stalls.rb, gvl_sleep.rb, measure_overhead.rb, minitest.rb, ractor.rb, rails.rb and threaded_http_requests.rb.

The C extension and the Ruby version floor

The repository layout shows where the work is. There is an ext/ directory, which is the native extension, alongside lib/ for the Ruby side, exe/ for the CLI entry point, bin/ for setup and console scripts, test/ for the test suite, examples/ and the gemspec, plus a Rakefile and a Gemfile. A.ruby-version file pins the version used in development, and a CODE_OF_CONDUCT.md is present.

The extension is not incidental. A sampling profiler needs to read the interpreter's own stacks, and in CRuby that means a C extension with access to internals. The ext/ directory is also why the Ruby version floor is where it is.

The gem is added to a Gemfile with a version constraint:

ruby
gem "vernier", "~> 1.0"

The maintenance state is healthy. Versions 1.9.0 came on 2025-11-27, 1.10.0 and 1.10.1 both on 2026-05-05, and the last push was on 2026-10-18 with the repository not archived. The licence is MIT, with LICENSE.txt at the top of the tree.

Development is a standard Rake setup: run bin/setup after checking out to install dependencies, rake test to run the tests, and bin/console for an interactive prompt.

The project also points at public profiling sessions, including a livestreamed demo and a RubyKaigi 2024 overview, plus shareable profiles of real workloads, which is a good way to see what the tool produces before you commit to it.

Editorial conclusion

Vernier suits a Ruby developer who wants to profile a live process rather than a benchmark, especially one running threads, a web server or a job queue, since it samples across threads and can record retained memory rather than only a time series. It is the wrong tool if you want deterministic benchmark numbers rather than a picture of what happened, and it needs Ruby 3.2.1 or newer on a Unix-like system. Before adopting it, read the two output modes carefully, because a retained memory flamegraph is read on a different axis from a time flamegraph, and confusing the two leads to the wrong conclusion.

Frequently asked questions

What is Vernier and what does it profile?

Vernier is a sampling profiler for Ruby 3.2.1 and newer that tracks multiple threads, GVL activity, GC pauses and idle time, and can also record retained allocations. It requires Ruby 3.2.1 or greater and a Unix-like operating system.

How do I install and use Vernier?

Add the gem with gem "vernier", "~> 1.0", then profile a command with vernier run -- ruby -e 'sleep 1', or a block with Vernier.profile(out: "time_profile.json") do ... end. The aliases Vernier.run and Vernier.trace are also available, along with Vernier.start_profile and Vernier.stop_profile.

How do I profile a running Rails or Rack app with Vernier?

Add Vernier::Middleware to config.ru, or config.middleware.use Vernier::Middleware in config/application.rb, optionally with a permit lambda that filters by PATH_INFO. You then enable profiling per request with query parameters such as vernier=true&vernier_interval=100.

How is a retained memory flamegraph different from a time one?

In a retained memory flamegraph the x-axis represents a proportion of memory in bytes rather than time or samples, and the topmost boxes on the y-axis are the retained objects with their stacktrace below, their width being the share of overall retained memory. The gc option, which defaults to true, runs a full GC cycle first.

What can I view a Vernier profile in?

In the web app at vernier.prof, locally using the profile-viewer gem, or with the vernier view command in the CLI. The web app and the profile-viewer gem are described as lightly customized versions of the Firefox profiler frontend that the profiles are compatible with.

Official sources

  1. jhawthorn/vernier on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jhawthorn-vernier.svg)](https://hysenlabs.com/projects/jhawthorn-vernier)

Was this useful?