# Stackprof: a sampling call-stack profiler for Ruby that stays out of the way

> A C extension that samples Ruby's call stack on a timer or on allocation, aggregates samples into a call graph, and reports the result as text, a dotgraph, source annotations or a flamegraph.

**tmm1/stackprof** — a sampling call-stack profiler for ruby 2.2+

- Repository: https://github.com/tmm1/stackprof
- Stars: 2,194 · Forks: 135
- Language: Ruby
- License: MIT
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/tmm1-stackprof

## Wrapping a block is the entire Ruby interface

Installation is one gem line, and profiling is one block:

```ruby
gem 'stackprof'
```

```ruby
StackProf.run(mode: :cpu, out: 'tmp/stackprof-cpu-myapp.dump') do
  #...
end
```

The block form is the important part of the design. Because profiling is scoped to a block rather than to the process, you can profile one request, one job or one script section inside an application that is otherwise running normally. That is a meaningful difference from profilers that need the whole VM started under instrumentation.

For Rack applications there is middleware instead, which is the form most Rails users will reach for:

```ruby
use StackProf::Middleware, enabled: true,
                           mode: :cpu,
                           interval: 1000,
                           save_every: 5
```

The `enabled` flag is what lets you ship the middleware and switch it on per environment rather than removing it, and `save_every` controls how often a dump is flushed, which matters for long-running processes where you want to inspect a result without waiting for a request to complete.

The requirements are short and one of them will rule the gem out immediately: Ruby 2.2 or newer, and a Linux-based operating system. There is no macOS or Windows support listed, which is a real constraint for a developer whose day job is on a Mac.

## Four sampling modes and what each one is actually measuring

The README lists four modes and, more helpfully, names the POSIX timer and Ruby event behind each one.

  - `:wall` uses `ITIMER_REAL` with `SIGALRM`, and is the default. Samples every _interval_ microseconds of wallclock time, defaulting to 1000.
  - `:cpu` uses `ITIMER_PROF` with `SIGPROF`. Samples every _interval_ microseconds of CPU activity, also defaulting to 1000, which is one millisecond.
  - `:object` uses `RUBY_INTERNAL_EVENT_NEWOBJ` and samples every _interval_ allocations, defaulting to 1.
  - `:custom` is user-defined through a direct `StackProf.sample` call.

The wall versus cpu distinction is the one that catches people. Wall time counts everything your process waited for, including time blocked on a database or an HTTP call. CPU time does not. If a request is slow because of an external service, only wall mode will show you, and only as a flat block with no interesting frames underneath it, which is a useful signal in itself.

Object allocation mode is the one people reach for when memory grows unexpectedly. The interval is in allocations rather than microseconds, so `interval: 1` samples every single allocation. The README also documents `ignore_gc`, which suppresses explicit garbage collection frames. By default GC time appears as mark and sweep frames, and on long traces those gaps make a flamegraph hard to read, so this option exists to trade explicitness for readability.

## Three Ruby C-APIs do the work

The README explains the mechanism, which is unusually clear for a profiler README and settles the question of how much overhead to expect.

Signal handlers enqueue a sampling job with `rb_postponed_job_register_one`. The point of deferring rather than sampling inside the handler is that the VM might be mid-garbage-collection or in some other inconsistent state when the signal arrives, and taking a call stack at that moment would not be safe.

Stack frames are then collected through `rb_profile_frames`, described as low-overhead C-API access to the VM call stack with no object allocations on that path. The no-allocation property is what makes object mode possible at all, since allocating to record allocations would be self-defeating.

In allocation mode, samples arrive through `rb_tracepoint_new(RUBY_INTERNAL_EVENT_NEWOBJ)`, a notification fired every time the VM allocates.

There is a version detail worth flagging here. The repository description and the requirements section both say Ruby 2.2+, but this paragraph attributes the three APIs to Ruby 2.1. Both statements can be true at once, since an API introduced in 2.1 is available in 2.2, so the requirement is a floor rather than a claim about where the APIs came from. Just be aware the README mixes a requirement line with a historical note in the same section.

## Samples aggregate into a call graph you can query

Each sample is a stack of frames, where a frame looks like `MyClass#method` or `block in MySingleton.method`. For every frame the profiler keeps four pieces of metadata: `samples` for times the frame was topmost, `total_samples` for times it appeared anywhere in the stack, `lines` broken down by line number, and `edges` counting calls from that frame into callees.

The README gives the aggregation as pseudocode, and it is short enough to be worth reading because it explains the whole model:

```ruby
trap('PROF') do
  top, *rest = caller

  top.samples += 1
  top.lines[top.lineno] += 1
  top.total_samples += 1

  prev = top
  rest.each do |frame|
    frame.edges[prev] += 1
    frame.total_samples += 1
    prev = frame
  end
end
```

The invariant that falls out of this is worth remembering when reading a report: on any frame, the sum of outbound edge weights equals the total samples on that frame. So a method that is expensive because of its callees, rather than its own body, looks expensive in the total column and cheap in the self column, and that difference is exactly what tells you whether to go read that method or the things it calls.

## Text reports drill from totals down to source lines

The text report is the reason to reach for this over a graphical tool, because it works over SSH on a box with no browser. A top-level summary looks like this:

```
$ stackprof tmp/stackprof-cpu-*.dump --text --limit 1
```

and the columns are `TOTAL (pct)` next to `SAMPLES (pct)` next to `FRAME`. The first is time spent anywhere in that frame's subtree, the second is time in the frame itself. That pairing is the core diagnostic: a large total with a small self means keep descending, a large self means stop here.

Drilling into a method gives you callers, callees and annotated source:

```
$ stackprof tmp/stackprof-cpu-*.dump --method 'String#blank?'
```

The annotated output interleaves sample counts with the original source lines, which is how you end up knowing that line 81 of a particular gem version is where the cost actually is. That is more actionable than a flamegraph when you are trying to decide whether to file an issue upstream.

The README also lists dotgraph and source annotation as supported reporting modes, and points at stackprof-webnav for an experimental web UI.

## Flamegraphs need raw mode collected in advance

Flamegraph support has a prerequisite that catches people out: the profile has to have been collected with `raw: true`, because the aggregate form does not retain what the flamegraph format needs. The README is explicit about this, and it means an existing dump cannot be converted.

Once you have a raw dump, three output paths exist. The built-in viewer writes a flamegraph file and then tells you the exact shell command to run:

```
$ stackprof --flamegraph tmp/stackprof-cpu-myapp.dump > tmp/flamegraph
```

```
$ stackprof --flamegraph-viewer=tmp/flamegraph
```

Or skip the viewer entirely and generate a self-contained HTML file using d3-flame-graph:

```
$ stackprof --d3-flamegraph tmp/stackprof-cpu-myapp.dump > flamegraph.html
```

The HTML route is the one to prefer on a remote server, since it needs nothing installed to view.

On project state: there are no published releases and the topics list is empty, though a `CHANGELOG.md` is present in the repository. The last push to the default branch was 2026-03-24, which puts it past the point where new platform support is likely to appear. With 2,195 stars and 135 forks against 59 open issues, the profile is a widely used tool whose open question count suggests an active user community rather than an actively released one. If you need a profiler that is being shipped on a schedule, that is not this; if you need one that works today on Linux with Ruby 2.2 or newer, it very likely is.

## Conclusion

Stackprof answers a narrow question well: where did the time actually go inside a running Ruby process, without asking you to change how the program is built. The Ruby and Rack entry points are three lines each, and the text report drills from a total column down to annotated source lines, which is enough to find the hot method without leaving the terminal. The limits are equally clear. It requires Linux, it has no published releases, its topics list is empty, and the last push was 2026-03-24, so treat the gem as stable rather than actively developed. Add it to a staging Gemfile, wrap the suspect request in StackProf.run, and read the report with --text and --limit before reaching for anything heavier.

## FAQ

### What is stackprof and what does it do?

Stackprof is a sampling call-stack profiler for Ruby, written as a C extension and inspired by gperftools. It samples the call stack on a timer or on object allocation, aggregates the samples into a call graph, and reports the result as text, a dotgraph, source annotations or a flamegraph.

### How do I generate a flamegraph with stackprof?

The profile must be collected with raw: true, since the aggregate dump does not retain what a flamegraph needs. Then run stackprof --flamegraph against the dump, and use stackprof --flamegraph-viewer to get the command that opens it, or stackprof --d3-flamegraph to produce a self-contained HTML file.

### Which platforms does stackprof support?

The README lists Ruby 2.2 or newer and a Linux-based operating system as the requirements, with no macOS or Windows support mentioned. macOS users who want a comparable sampling view can use rbtrace, and ruby-prof is the usual alternative when you want instrumentation rather than sampling.

### What is the difference between wall and cpu mode in stackprof?

Wall mode samples elapsed wallclock time using ITIMER_REAL and SIGALRM, so it includes time the process spent waiting on a database or network call. CPU mode uses ITIMER_PROF and SIGPROF and samples CPU activity only, so blocking waits do not show up. Both default to an interval of 1000 microseconds.

## Sources

- [Issues](https://github.com/tmm1/stackprof/issues)
- [License: MIT](https://github.com/tmm1/stackprof/blob/master/LICENSE)
- [README](https://github.com/tmm1/stackprof/blob/master/README.md)
- [tmm1/stackprof on GitHub](https://github.com/tmm1/stackprof)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tmm1-stackprof
