Skip to content

ci: make the CodSpeed memory benchmarks deterministic - #298

Merged
alexander-akait merged 5 commits into
mainfrom
claude/update-deps-github-actions-5ydose
Sep 26, 2026
Merged

alexander-akait merged 5 commits into
mainfrom
claude/update-deps-github-actions-5ydose

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Summary

CodSpeed's memory benchmarks report different numbers for the same code. On #297, which only changed tests, CodSpeed flagged 16 memory regressions of up to -90%.

Cause: the memory job ran without the V8 flags CodSpeed requires for its analysis modes. Every run's log said:

[CodSpeed] missing required flags: --interpreted-frames-native-stack, --allow-natives-syntax, --hash-seed=1, --random-seed=1, --no-opt, --predictable, --predictable-gc-schedule, --no-concurrent-sweeping

Without these flags, V8 optimizes on background threads, and TurboFan's zone allocations land in whichever benchmark is being measured at the time.

Evidence (local)

I counted native allocations inside each measured window, with CodSpeed's start/stop hooks marking the window. Each configuration ran 3 times on 1 CPU and 3 times on 4 CPUs:

benchmarks that varied examples
without the flags 38 / 38 replace-source: source() 576 B – 345 KB, original-source: map({ columns: true }) 1.86 – 2.27 MB
with the flags 30 / 38, by a small margin large benchmarks agree within ~0.3%; tiny ones still move by up to ~1.3 KB, mostly from ASLR

Without the flags, backtraces show about 3,500 TurboFan zone segments inside the measured windows (OptimizingCompileDispatcher::CompileTask → Zone::Expand). With the flags there are none. On this PR, the memory job no longer prints the warning.

Changes

  • benchmark:memory passes the same V8 flags as benchmark.
  • The benchmark jobs pin the runner image to ubuntu-24.04, which is what ubuntu-latest resolves to today. A base run and a head run on either side of an image update would otherwise shift every benchmark at once. Node.js stays on lts/*, so Node.js improvements show up on the dashboard.
  • The CodSpeed wrapper logs the mode it actually runs in; it always said "simulation" before.
  • The benchmark README explains why benchmark:memory needs the flags.

Not in this PR: simulation-mode variance

CPU (simulation) benchmarks also moved by up to ±35% between two main commits that only changed tests. The short ones are dominated by harness overhead: the gc() right before each measured call, plus the call wrapper. I tried two changes to when that gc() runs. Both were deterministic locally under callgrind, but on CodSpeed they made short benchmarks slower (for example new RawSource(buffer) went from 70 µs to 938 µs, with a flame graph dominated by runtime internals), and my local runs don't reproduce that. Both are reverted here; this needs a separate investigation.

Expected CodSpeed result on this PR

main's memory numbers were measured without the flags, so this PR's report shows large memory changes in both directions once. Stability shows up in the runs after this one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F

The memory job ran without the V8 flags CodSpeed requires for its
analysis modes; its log says so on every run ("[CodSpeed] missing
required flags: ... --no-opt, --predictable, ..."). Without them V8
optimizes on background threads, and TurboFan's zone allocations land in
whichever benchmark is being measured, so the same code reported swings
of up to several MB between runs. That is what produced the memory
"regressions" on pull requests that only touched tests.

Measured locally by counting native allocations inside each measured
window (1 vs 4 CPUs, 3 runs each): without the flags every one of the
38 memory benchmarks varied, with about 3,500 TurboFan zone segments
landing in the windows; with them, none do, and the large benchmarks
agree within ~0.3%.

- `benchmark:memory` passes the same flags as `benchmark`.
- The benchmark jobs pin `ubuntu-24.04` and Node.js 24.21.0, the
  environment `ubuntu-latest` and `lts/*` resolve to today, so base and
  head runs no longer straddle an image or Node.js update (Node.js 26
  becomes `lts/*` in October).
- The wrapper logs the actual CodSpeed mode instead of always
  "simulation".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F
@changeset-bot

changeset-bot Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 182e3e7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@linux-foundation-easycla

linux-foundation-easycla Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

CLA Not Signed

One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via:

Co-authored-by: name <email>

Supported Co-authored-by: formats include:

  1. Anything <id+login@users.noreply.github.com> - it will locate your GitHub user by id part.
  2. Anything <login@users.noreply.github.com> - it will locate your GitHub user by login part.
  3. Anything <public-email> - it will locate your GitHub user by public-email part. Note that this email must be made public on Github.
  4. Anything <other-email> - it will locate your GitHub user by other-email part but only if that email was used before for any other CLA as a main commit author.
  5. login <any-valid-email> - it will locate your GitHub user by login part, note that login part must be at least 3 characters long.

Alternatively, if the co-author should not be included, remove the Co-authored-by: line from the commit message.

Please update your commit message(s) by doing git commit --amend and then git push [--force] and then request re-running CLA check via commenting on this pull request:

/easycla

Node.js updates can make the library faster or leaner, and the
dashboard should show that, so the benchmarks keep following `lts/*`.
Only the runner image stays pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F
@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 64cefcd0-052e-49f9-ac26-8f21268a9272

📥 Commits

Reviewing files that changed from the base of the PR and between 2d3664d and 4c424f1.

📒 Files selected for processing (1)
  • benchmark/with-codspeed.mjs

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.


Walkthrough

The benchmark and memory-benchmark jobs now use the pinned Ubuntu 24.04 runner image. The memory benchmark script uses deterministic V8 flags. The CodSpeed wrapper instruments memory mode and logs the selected mode. It performs pre-measurement garbage collection only in memory mode. The local-runner documentation describes the required flags and their effect on memory results.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 4c424

No actionable merge-blocking issue is evident; the benchmark changes are ready for normal checks.

Architecture Summary

Architecture risk: 🔵 Low · up to 4c424

The change affects 2 systems.

Changed systems: benchmark, package.json

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — benchmark (service) was modified; 2 changed files map to changed impact.
  • observed — package.json (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in benchmark/README.md: The documentation expands the required V8 flags from CodSpeed’s instrumentation mode to instrumentation and memory modes. It describes how background-thread optimization can make compiler allocations land in different benchmarks, causing memory results to vary between runs, and notes CodSpeed’s warning when a flag is missing.
  • observed — Modified behavior in package.json: benchmark:memory replaces its prior --expose-gc --max-old-space-size=4096 invocation with the deterministic runtime flags used by benchmark, retaining the memory benchmark target and 4096 MB heap limit.
  • observed — Modified behavior in benchmark/with-codspeed.mjs: The mode documentation describes memory mode as using simulation behavior while CodSpeed tracks allocations.
  • observed — Modified behavior in benchmark/with-codspeed.mjs: The instrumentation comment now identifies both simulation and memory modes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: making CodSpeed memory benchmarks deterministic. It matches the V8 flag, garbage-collection, and documentation updates.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Member Author

EasyCLA fails on this PR because of the Co-Authored-By: Claude trailer on its commits. The Claude account can't sign a CLA, so a maintainer needs to override the check, as on the earlier PRs (#293, #297). The code changes don't affect this check.


Generated by Claude Code

@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.10%. Comparing base (d3548f7) to head (182e3e7).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #298   +/-   ##
=======================================
  Coverage   99.10%   99.10%           
=======================================
  Files          27       27           
  Lines        6337     6337           
  Branches      812      812           
=======================================
  Hits         6280     6280           
  Misses         56       56           
  Partials        1        1           
Flag Coverage Δ
integration 99.10% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed

codspeed Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 11 benchmarks

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 37 improved benchmarks
❌ 11 regressed benchmarks
✅ 164 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Memory concat-source memory: source() concatenates children 115.4 KB 345.1 KB -66.57%
❌ Memory cached-source memory: warm sourceAndMap() returns cached references 472 B 992 B -52.42%
❌ Memory original-source memory: map({ columns: true }) builds full mappings 468.7 KB 872.2 KB -46.26%
❌ Memory original-source memory: sourceAndMap({ columns: true }) 468.8 KB 872.1 KB -46.25%
❌ Simulation raw-source: buffer() (from buffer) 226.8 µs 325.4 µs -30.29%
❌ Memory compat-source memory: delegated source() + map() through wrapper 9.7 KB 13.3 KB -27.12%
❌ Simulation source-map-source: map() 612.7 µs 755.2 µs -18.86%
❌ Simulation raw-source: buffers() cached 465.3 µs 545.9 µs -14.77%
❌ Simulation prefix-source: new PrefixSource(str, string) 378.8 µs 443.4 µs -14.56%
❌ Simulation prefix-source: new PrefixSource(str, Source) 316.8 µs 370.5 µs -14.5%
❌ Simulation source-map-source: buffers() (from buffer) 420.5 µs 473.6 µs -11.21%
⚡ Memory replace-source memory: construct + 100 insertions 68.6 KB 1.3 KB ×54
⚡ Memory size-only-source memory: new SizeOnlySource() 758.2 KB 100 KB ×7.6
⚡ Memory concat-source memory: new ConcatSource(...children) 5.1 KB 1.3 KB ×4
⚡ Memory replace-source memory: map({ columns: true }) splices mappings 3,560.1 KB 921.1 KB ×3.9
⚡ Memory replace-source memory: source() concatenates result 5.2 KB 1.9 KB ×2.7
⚡ Memory cached-source memory: getCachedData() allocates BufferedMap 1,392 B 664 B ×2.1
⚡ Memory raw-source memory: updateHash() populates _cachedHashUpdate 195.2 KB 104.5 KB +86.76%
⚡ Memory clear-cache memory: unique tasks (clearCache default) 4.9 MB 3.1 MB +56.88%
⚡ Memory clear-cache memory: unique tasks (clearCache maps + parsedMap, keep source) 4.9 MB 3.1 MB +56.04%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/update-deps-github-actions-5ydose (182e3e7) with main (d3548f7)

Open in CodSpeed

The CodSpeed wrapper ran gc() twice right before every measured call.
Memory mode needs that, so warmup garbage is not counted, but in
simulation mode it puts the heap's re-setup into the measured call:
under callgrind, raw-source benchmarks spend 35-60k more instructions
with it (30-50% of the short ones, e.g. isBuffer() 198,960 vs 144,525),
and on CodSpeed that is the part that moved between runs of identical
code, e.g. the one RawSource constructor in isBuffer() measured 114 us
in one run and 211 us, with syscalls, in the next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F
Measuring right after gc() (main) puts the heap's re-setup into the
measured call; not collecting at all (previous commit) lets garbage from
earlier tasks trigger a collection inside it, so a task's result
depends on the tasks that ran before it (concat-source buffers() nested
went 326k -> 768k instructions, 1.7ms on CodSpeed). Collect, then run
the body once more before measuring.

Under callgrind, full cases suite (174 tasks):

                        run-to-run   depends on preceding suites
  gc before measuring   1.18%        14/39 tasks >1%, max 46%
  no gc                 0.26%        27/39 tasks >1%, max 171%
  gc + one more run     0.07%         7/39 tasks >1%

Memory mode is unchanged: it collects right before the measured call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F
The two previous commits changed when simulation mode collects garbage.
Locally under callgrind the last variant was the most stable, but on
CodSpeed it made short benchmarks much slower (new RawSource(buffer)
70 us -> 938 us, flame graph dominated by runtime internals), which
the local runs do not reproduce. Restore main's behavior so this pull
request only carries the verified memory-mode fix; the simulation-mode
variance needs its own investigation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThsbPWvW5vnYWEaFs2Q22F

Copy link
Copy Markdown
Member Author

CodSpeed Performance Analysis is red against main. That's the expected one-time shift: main's memory numbers were measured without the required V8 flags.

The two CodSpeed runs on this PR show whether the numbers are now stable. 2d3664d and 182e3e7 use identical benchmark code, so any difference between them is noise. They also ran on different hardware: the memory job moved from AMD EPYC 7763 to Intel Xeon 6973P-C, and the CPU job from EPYC 7763 to EPYC 9V74. Comparing them in CodSpeed:

  • 207 of 212 benchmarks unchanged, including every CPU benchmark and every memory benchmark above 5 KB.
  • 5 changed: only tiny memory benchmarks, each by 0.5–1 KB (for example warm sourceAndMap() went from 8 B to 992 B). That matches the remaining noise I measured locally, which comes from V8 allocating heap-page metadata.

For comparison, the #297 run against its base showed 16 memory regressions of up to 90%, including benchmarks in the MB range.

A maintainer needs to acknowledge this report on CodSpeed; I can't do that from here.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit 4f49c30 into main Sep 26, 2026
34 of 37 checks passed
@alexander-akait
alexander-akait deleted the claude/update-deps-github-actions-5ydose branch September 26, 2026 17:10
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.

2 participants