Skip to content

1.2.2: faster builds (working directory read once, header listings remembered, one mock collection per test) - #1383

Merged
mkarlesky merged 10 commits into
masterfrom
fix/1.2.2-build-speed
Oct 31, 2026
Merged

mkarlesky merged 10 commits into
masterfrom
fix/1.2.2-build-speed

Conversation

@mkarlesky

Copy link
Copy Markdown
Member

Summary

External tests on complex projects ran at 1.2.1's speed or slightly slower, even though the TEST_SOURCE_FILE() fix should have made them faster. Profiling master against v1.2.1 found three causes, and this PR fixes all three. Every measured run is now faster than on both master and v1.2.1, and generated mocks are byte-identical to master's.

  1. The working directory is read once. ProjectPaths asked the operating system for the working directory (Dir.pwd) on every path lookup, cache hits included. That's about 15,000 calls for one test and 45,000–79,000 for a 200-test build, at 20–70 µs each on macOS. 1.2.1 made 3 calls a run. This was the one new cost since 1.2.1.
    • The new WorkingDir module (lib/ceedling/filesystem/working_dir.rb) reads the directory once.
    • WorkingDir.chdir is the one sanctioned way to change directory: it forgets the remembered directory going in and coming out. The Dependencies plugin's commands and FileSystemWrapper#cd go through it.
    • The specs change directory in-process, so spec/support/working_dir_tracking.rb forgets the remembered directory on every Dir.chdir, in specs only.
  2. Header listings are remembered for a test build. Finding a mock's real header listed every directory on the test's search paths afresh, 3 times a test plus once per mock. That was about a third of a no-op rebuild, and it predates 1.2.1.
    • IncludePathinator#ordered_header_files keeps each directory's listing for one test build. Ordering, de-duplication and every caller are unchanged.
    • Directories under the build root are listed on every call, since the build writes there.
    • TestInvoker forgets the listings at the start of each test build, after the pre_test_build hook.
  3. The planner builds one mock header collection per test. Stage 5 rebuilt the collection inside its loop over a test's mocks, though nothing in that loop changes the search paths. It's now built at the first real mock and shared by the rest; a test with only Partial mocks builds none.

Headers that a hook generates outside the build root partway through a build are out of scope; that's tracked as its own issue. There's no Changelog entry: the working-directory cost arrived during this prerelease cycle, and the other two fixes change nothing a user configures.

Measurements

Medians of interleaved runs on macOS, Ruby 3.2.2. The synthetic project has 200 tests, two mocks and a TEST_SOURCE_FILE() per test, and 210 include directories.

Run v1.2.1 master this PR
Single test, preprocessing 1.48 s 1.84 s 1.27 s
No-op rebuild, preprocessing 79.0 s 38.3 s 24.7 s
Single test, no preprocessing 1.16 s 1.29 s 0.91 s
No-op rebuild, no preprocessing 19.3 s 19.2 s 11.1 s
Clean build, no preprocessing 122.7 s 120.8 s 111.6 s
temp_sensor no-op rebuild 2.58 s 2.73 s 2.46 s

Share of the change: the working-directory memo removes the slowdown since 1.2.1, the header listings give about a third off every no-op rebuild, and the planner change adds a few percent. On Linux a Dir.pwd call costs about 1 µs, so the first fix matters less there.

Test plan

  • Each fix lands as a failing spec commit, then the fix. Each spec failed for the reason it names before its fix.
  • Mutation checks: removing either half of WorkingDir.chdir's forgetting, or any branch of the header-listing logic, fails specs.
  • Unit tier (5,310 examples) and integration tier (578) pass locally.
  • A full RuboCop run is clean, and the regenerated to-do is unchanged apart from its timestamp.
  • Synthetic projects (with and without preprocessing) and temp_sensor: all tests pass, and generated mocks are byte-identical to master's.
  • Docker system specs and the plugin tests on Linux, macOS and Windows (CI).

🤖 Generated with Claude Code

mkarlesky and others added 9 commits October 31, 2026 08:49
ProjectPaths reads Dir.pwd on every lookup, cache hits included. The specs
ask for the working directory to be read once, and add WorkingDir.chdir,
which answers for the directory it changes to and then for the one it left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProjectPaths asked the operating system for the working directory on every
path lookup: tens of thousands of times a build, at 20-70 us each on macOS.
WorkingDir now reads it once. Ceedling changes directory only through
WorkingDir.chdir, which forgets it going in and coming out, and the
Dependencies plugin uses it. The specs, which change directory in-process,
forget it on every Dir.chdir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mock's header is found by listing every header on its test's search paths,
and those directories are listed again on every call. The specs ask for a
directory outside the build root to be listed once per test build, for one
under the build root to be listed every time, and for each test build to
start with fresh listings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finding a mock's header listed every directory on its test's search paths
afresh, three times a test plus once per mock: a third of a no-op rebuild.
IncludePathinator now keeps each directory's listing for the test build.
Directories under the build root are listed every time, since the build
writes there, and TestInvoker starts each test build with fresh listings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The planner rebuilt a test's mock header collection for every mock it
resolved, though nothing in that loop changes the test's search paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Planning rebuilt the mock header collection for every mock a test includes.
It is now built at the first real mock and shared by the rest; a test with
only Partial mocks builds none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FileSystemWrapper#cd, which plugins can reach, used FileUtils.cd and so
left the remembered working directory stale inside its block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

CI summary · e615c23 · ✅ 25/25 jobs passed · nearest baseline 4f6f1b6 (master)

⚠️ Windows 3.0 units: pending 1 → 2
⚠️ Windows 3.1 units: pending 1 → 2
⚠️ Windows 3.2 units: pending 1 → 2
⚠️ Windows 3.3 units: pending 1 → 2
⚠️ Windows 3.4 units: pending 1 → 2

Tests (Linux 3.3) Examples Failures Pending
units 5311 (+332) 0 0
integration 578 (+67) 0 19
system 406 (+26) 0 35
Coverage Lines Δ
units 90.58% ▲1.05
integration 51.52% ▲0.91
system 83.89% ▲0.20
combined 98.53% ▲0.11

Diff coverage: 100.00% (37/37 changed lines).

RuboCop to-do Count Δ
offenses 755 —
cops 10 —
All legs (examples / failures / pending)
Leg units integration system
Linux 3.0 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 35
Linux 3.1 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 35
Linux 3.2 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 35
Linux 3.3 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 35
Linux 3.4 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 35
Linux 3.5 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 35
Windows 3.0 5309 / 0 / 2 577 / 0 / 19 406 / 0 / 62
Windows 3.1 5309 / 0 / 2 577 / 0 / 19 406 / 0 / 62
Windows 3.2 5309 / 0 / 2 577 / 0 / 19 406 / 0 / 62
Windows 3.3 5309 / 0 / 2 577 / 0 / 19 406 / 0 / 62
Windows 3.4 5309 / 0 / 2 577 / 0 / 19 406 / 0 / 62
macOS 3.1 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 51
macOS 3.2 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 51
macOS 3.3 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 51
macOS 3.4 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 51
macOS 3.5 5311 / 0 / 0 578 / 0 / 19 406 / 0 / 51
Jobs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mkarlesky
mkarlesky merged commit 3b30ae3 into master Oct 31, 2026
7 of 26 checks passed
@mkarlesky
mkarlesky deleted the fix/1.2.2-build-speed branch October 31, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant