Skip to content

Commit bab0bb7

Browse files
committed
ralph: add bootsnap and spring for faster test loading
Investigated slow unit test load times (~20s to load, 1.7s to run). Root causes: eager loading of ~1400 files, no caching, misconfigured spring. Changes: - Add bootsnap gem for require caching (30-50% speedup expected) - Configure bootsnap in config/boot.rb - Create config/spring.rb for proper Spring preloader setup - Document findings in docs/internal/test_performance.md
1 parent ac073b6 commit bab0bb7

File tree

6 files changed

+222
-4
lines changed

6 files changed

+222
-4
lines changed

.ralph/progress.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
55
## Summary
66

7-
- Iterations completed: 0
8-
- Current status: Initialized
7+
- Iterations completed: 1
8+
- Current status: Implementation complete, needs verification
99

1010
## How This Works
1111

@@ -18,3 +18,33 @@ This is how Ralph maintains continuity across iterations.
1818

1919
### 2026-01-22 14:41:38
2020
**Session 1 started** (model: opus-4.5)
21+
22+
### 2026-01-22 (Iteration 1)
23+
**Completed:**
24+
- Analyzed test loading performance, identified root causes:
25+
- Eager loading of ~1400 Ruby files on every test run
26+
- No bootsnap caching
27+
- Spring not properly configured
28+
- Outdated Spork code still present
29+
- Created comprehensive documentation at `docs/internal/test_performance.md`
30+
- Added bootsnap to Gemfile and configured in `config/boot.rb`
31+
- Created `config/spring.rb` for proper Spring configuration
32+
33+
**Changes made:**
34+
- `Gemfile`: Added bootsnap gem
35+
- `config/boot.rb`: Added bootsnap initialization
36+
- `config/spring.rb`: Created Spring configuration
37+
- `docs/internal/test_performance.md`: Created documentation
38+
- `RALPH_TASK.md`: Checked off criteria 1 and 2
39+
40+
**Blocked:**
41+
- Cannot run tests to verify speedup - Ruby environment not properly set up (system Ruby 2.6.10 instead of required 3.2.10)
42+
- Need to run `bundle install` in proper Ruby environment, then test with:
43+
```
44+
time bundle exec rspec spec/unit/actions/app_create_spec.rb
45+
```
46+
47+
**Next steps:**
48+
- Install bundle in proper Ruby environment
49+
- Run test and measure improvement
50+
- Mark criterion 3 complete if successful

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ group :test do
9292
end
9393

9494
group :development do
95+
gem 'bootsnap', require: false
9596
gem 'byebug'
9697
gem 'debug', '~> 1.11'
9798
gem 'listen'

RALPH_TASK.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ Running spork and/or spring doesn't seem to help substantially, but maybe they'r
2222

2323
## Success Criteria
2424

25-
1. [ ] Document exists explaining why tests are slow
26-
2. [ ] Document explains solution(s) to why tests are slow
25+
1. [x] Document exists explaining why tests are slow
26+
2. [x] Document explains solution(s) to why tests are slow
2727
3. [ ] Tests reliably load/run faster
2828

2929
---

config/boot.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,19 @@
55
$LOAD_PATH.unshift(File.expand_path('../middleware', __dir__))
66

77
require 'bundler/setup' # Set up gems listed in the Gemfile.
8+
9+
# Speed up boot time by caching require calls
10+
if ENV.fetch('DISABLE_BOOTSNAP', nil).nil?
11+
begin
12+
require 'bootsnap'
13+
Bootsnap.setup(
14+
cache_dir: File.join(__dir__, '..', 'tmp', 'cache'),
15+
development_mode: ENV.fetch('RAILS_ENV', 'development') != 'production',
16+
load_path_cache: true,
17+
compile_cache_iseq: true,
18+
compile_cache_yaml: true
19+
)
20+
rescue LoadError
21+
# bootsnap is optional, continue without it
22+
end
23+
end

config/spring.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Spring.application_root = File.expand_path('..', __dir__)
2+
3+
# Files that should trigger a restart when changed
4+
Spring.watch(
5+
'.ruby-version',
6+
'Gemfile',
7+
'Gemfile.lock',
8+
'config/cloud_controller.yml'
9+
)
10+
11+
# Directories to watch for changes
12+
%w[
13+
lib
14+
app
15+
config
16+
spec/support
17+
].each do |path|
18+
Spring.watch(path)
19+
end

docs/internal/test_performance.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Unit Test Performance Analysis
2+
3+
This document explains why unit tests in cloud_controller_ng take a long time to load and proposes solutions.
4+
5+
## The Problem
6+
7+
Running a single unit test like `bundle exec rspec spec/unit/actions/app_create_spec.rb` takes approximately 20 seconds to load and only ~1.7 seconds to actually run. The load time dominates total test time.
8+
9+
## Root Causes
10+
11+
### 1. Eager Loading of the Entire Application (~1400 Ruby files)
12+
13+
The `spec_helper.rb` requires `cloud_controller.rb` which triggers a cascade of requires:
14+
15+
- `cloud_controller.rb` loads ~120 files directly from `lib/`
16+
- `cloud_controller/controllers.rb` loads all 117 controller files via `Dir[]`
17+
- `models.rb` loads 162 model files
18+
- `services.rb` loads service-related code
19+
- All 105 support files in `spec/support/` are loaded via `Dir[]`
20+
21+
This eager loading happens on every test run, even for a single spec file.
22+
23+
### 2. No Bootsnap Caching
24+
25+
[Bootsnap](https://github.com/shopify/bootsnap) is a library that caches `require` calls (both the file path resolution and the compiled instruction sequences). It can reduce boot time by 50-70% for large Ruby applications.
26+
27+
This project does not use bootsnap.
28+
29+
### 3. Spring Not Properly Configured
30+
31+
Spring and spring-commands-rspec are in the Gemfile, but Spring is not configured:
32+
33+
- No `config/spring.rb` file exists to specify what to watch
34+
- Spring doesn't know which directories to preload
35+
- Without configuration, Spring may not preload the application effectively
36+
37+
The bin/rspec attempts to use Spring, but without proper configuration, it may not be helping.
38+
39+
### 4. Spork is Outdated and Ineffective
40+
41+
The spec_helper.rb contains code to support Spork, but:
42+
43+
- Spork is loaded from an old git ref and is no longer maintained
44+
- The implementation checks if spork is running via `ps | grep spork` which adds overhead
45+
- Spork has been superseded by Spring
46+
47+
### 5. Database Setup on Every Run
48+
49+
`SpecBootstrap.init` is called on every test run with:
50+
51+
- `recreate_test_tables: true` (default) - recreates database tables
52+
- `do_schema_migration: true` (default) - runs migration checks
53+
54+
These database operations add to startup time.
55+
56+
## Solutions
57+
58+
### Solution 1: Add Bootsnap (Recommended - Low Risk)
59+
60+
Bootsnap can be added with minimal changes and provides significant speedup.
61+
62+
**Implementation:**
63+
64+
1. Add bootsnap to Gemfile:
65+
66+
```ruby
67+
group :development, :test do
68+
gem 'bootsnap', require: false
69+
end
70+
```
71+
72+
2. Add to `config/boot.rb` before other requires:
73+
74+
```ruby
75+
require 'bootsnap/setup' if ENV.fetch('DISABLE_BOOTSNAP', nil).nil?
76+
```
77+
78+
**Expected benefit:** 30-50% reduction in boot time with no risk to test behavior.
79+
80+
### Solution 2: Properly Configure Spring (Recommended - Medium Risk)
81+
82+
Create a proper Spring configuration so the preloader works effectively.
83+
84+
**Implementation:**
85+
86+
1. Create `config/spring.rb`:
87+
88+
```ruby
89+
Spring.application_root = File.expand_path('..', __dir__)
90+
91+
Spring.watch(
92+
'.ruby-version',
93+
'Gemfile',
94+
'Gemfile.lock'
95+
)
96+
97+
# Watch the lib directory for changes
98+
%w[lib app spec/support].each do |path|
99+
Spring.watch path
100+
end
101+
```
102+
103+
2. Regenerate binstubs:
104+
105+
```bash
106+
bundle exec spring binstub rspec
107+
```
108+
109+
3. Verify Spring is running:
110+
111+
```bash
112+
bin/spring status
113+
```
114+
115+
**Expected benefit:** After first run, subsequent runs should complete in 1-3 seconds.
116+
117+
### Solution 3: Remove Spork (Cleanup - Low Risk)
118+
119+
Remove the outdated Spork code from spec_helper.rb to simplify the codebase.
120+
121+
**Implementation:**
122+
123+
Remove lines 5-13 and 225-239 from spec_helper.rb that deal with Spork, keeping only the `init_block.call` and `each_run_block.call` lines.
124+
125+
### Solution 4: Skip Database Migration Checks
126+
127+
Use the `NO_DB_MIGRATION=true` environment variable:
128+
129+
```bash
130+
NO_DB_MIGRATION=true bundle exec rspec spec/unit/actions/app_create_spec.rb
131+
```
132+
133+
This skips migration checks when the schema is known to be up-to-date.
134+
135+
**Expected benefit:** Saves 1-2 seconds per run.
136+
137+
## Recommended Approach
138+
139+
1. **First**: Add bootsnap (lowest risk, immediate benefit)
140+
2. **Second**: Configure Spring properly (requires testing, but greatest benefit for iterative development)
141+
3. **Third**: Remove Spork code (cleanup)
142+
4. **Optional**: Use NO_DB_MIGRATION when appropriate
143+
144+
## Verification
145+
146+
After implementing changes, measure improvement with:
147+
148+
```bash
149+
time bundle exec rspec spec/unit/actions/app_create_spec.rb --format progress
150+
```
151+
152+
Compare the total time before and after each change.

0 commit comments

Comments
 (0)