testprof
TestProf: RSpec Performance Optimization
Expert guidance for diagnosing and fixing slow RSpec test suites. Based on field-tested techniques from Evil Martians' work reducing a 30,000-spec Rails suite from 4m30s to 2m (and targeting 1m30s with parallelization).
The core idea: slow test suites are rarely slow for one reason. They're slow for a handful of fixable reasons, stacked. This skill walks you through diagnosing which reasons apply, then fixing each one — in an order that avoids wasted effort.
How to use this skill
When a user brings a slow RSpec suite, investigate their actual codebase before prescribing anything. A skill full of generic advice is worse than useless — the value is in mapping the general patterns onto their specific code. Follow this order:
- Characterize the problem — ask for total runtime, example count, and whether it's "slow everywhere" or "slow in specific files." This determines which profiler to run first.
- Investigate concretely, don't speculate. Before giving advice, actually:
- Read
spec/spec_helper.rbandspec/rails_helper.rbfor suspicious requires (rubocop/rspec/support, SimpleCov config, global hooks). - Grep the
Gemfilefor the suspects: SimpleCov, blind_index, any encryption gem, retry gems (retriable, Faraday retry middleware). - For slow individual specs, read the service file being tested — grep for
sleep,retry,ObjectSpace,create_list(:...,with large numbers. - Look at the factory definitions for the types involved. Association fan-out is a common cause. This is the difference between a suggestion list and a diagnosis. Always do the investigation first.
- Read
- Run the right diagnostic next — see "Diagnostic decision tree" below. If Claude can't run the profiler (no shell access to the user's env), write the exact commands the user should run and explain how to interpret the output.
- Apply fixes in order of payoff — coverage and GC issues often give the biggest single wins and require the least invasive changes, so check them before rewriting specs.
- Measure after each change — keep the before/after numbers so you can tell what worked.
The four findings in references/bottlenecks.md cover the most common heavy hitters. Read that file when you're ready to dig into a specific bottleneck.
A worked investigation looks like this
Bad (generic suggestion list):
"Your service might be sleeping in tests. You could inject a delay parameter, or stub sleep, or use Retriable with base_interval: 0..."
Good (grounded in the user's code):
"I read
app/services/your_service.rb— the retry is hand-rolled at line 42 (sleep(2 ** attempt)), not delegated to a gem. So injection is the right fix, not gem config. Here's the diff..."
The second version took one Read call and one Grep but produced a fix the user can apply immediately. Always aim for the second version.
Diagnostic decision tree
Use the symptoms the user describes to pick the first profiler. Don't try to run all of them — start with the one most likely to surface signal, then pivot based on what you learn.
| Symptom | First thing to run | What it tells you |
|---|---|---|
| "Slow everywhere, no obvious hotspot" | TEST_STACK_PROF=1 bin/rspec (StackProf) or Vernier |
Global issues: gems loading heavy hooks, coverage, autoloading |
| "Specific files/groups are slow" | TPS_PROF=20 TPS_PROF_MIN_EXAMPLES=5 bin/rspec |
Tests-per-second for each file; surfaces low-TPS groups |
| "GC time looks high" | TEST_MEM_PROF=gc bin/rspec |
Per-spec GC overhead. Healthy is ~5%; investigate at 15%+ |
| "Want to know which spec types cost the most" | TAG_PROF=type bin/rspec (TagProf) |
Aggregate time by tag (model, request, system, etc.) |
| "Specific slow spec, want a line-level view" | RUBY_PROF=1 bin/rspec path/to/spec.rb |
Method-level hotspots inside one example |
Rule of thumb: if the user hasn't profiled yet, start with StackProf and a TPS run. Global issues and slow-file issues are usually independent problems.
Quick install
TestProf v1.6+ has all the profilers referenced here:
# Gemfile
group :test do
gem "test-prof", "~> 1.6"
end
Most profilers activate via environment variables — no code changes needed. For let_it_be, before_all, and FactoryProf, add the relevant requires to spec/spec_helper.rb (see references/testprof_features.md).
The four big wins
From a real 30k-spec suite, these accounted for the majority of the speedup. Any of them could apply to the user's suite. See references/bottlenecks.md for the full diagnostic steps and code for each.
- RuboCop
rubocop/rspec/supportcausing GC bloat — global hooks keep memory allocated across every spec. Replace with narrower requires. (~1 minute saved on the reference suite.) - Coverage tracking overhead (15-20%) — switch SimpleCov to
oneshot_linemode. (~1 minute saved.) - Encryption/blind_index using production parameters in tests — override to
pbkdf2_sha256withcost: 1in the test environment. (~30s saved.) - Individual slow specs found via TPS — patterns include ObjectSpace iteration, production retry backoffs, excessive record creation for boundary tests. Each fix is local but compound.
When to reach for let_it_be vs let
let_it_be (from TestProf) memoizes across an entire describe block instead of per-example. For read-only records used across many examples, this is a massive win:
# Before: creates a user for every example
let(:user) { create(:user) }
# After: creates once for the whole describe block
let_it_be(:user) { create(:user) }
Use let_it_be when the record doesn't need to be fresh per-example. If examples mutate it, either keep let or use let_it_be(:user, reload: true) / refind: true.
See references/testprof_features.md for the full set of TestProf primitives (before_all, AnyFixture, FactoryProf, EventProf, sample modes).
Tests-per-second (TPS): the metric that matters
Total runtime hides the real problem. A file with 1,080 examples that runs in 2m50s is 6 TPS — terrible. A file with 5 examples that runs in 10s is 0.5 TPS — worse per-example, but maybe unavoidable.
TPS prioritizes files where high runtime meets high example count. That's where refactoring pays off most:
# Show the 20 slowest files with at least 5 examples
TPS_PROF=20 TPS_PROF_MIN_EXAMPLES=5 bin/rspec
When a file shows up in TPS, read references/slow_spec_patterns.md for the common root causes (ObjectSpace walks, sleep/retry delays, oversized fixtures).
Preventing regressions
After optimizing, the suite will grow. To keep gains:
- Enforce in review: code review (human or AI) should flag
letthat could belet_it_be, tests that create hundreds of records for pagination/boundary checks, and reintroduction ofrequire "rubocop/rspec/support". - Track TPS over time: the
test-queueparallelizer plus periodic TPS runs will surface regressions. - CI budget: pick a target runtime and fail CI when it slips.
References
Read these when you need more depth on a specific topic:
references/bottlenecks.md— the four major bottlenecks with full diagnosis steps, code, and expected impactreferences/testprof_features.md— complete TestProf v1.6 feature reference (let_it_be,before_all, profilers,AnyFixture, FactoryProf, EventProf)references/slow_spec_patterns.md— common root causes inside individual slow specs, with before/after codereferences/diagnostic_scripts.md— drop-in snippets: GC tracker, timing decorator, suite summary hook