Skip to content

scripts: stop the slow-test parsers charging batch phases to the preceding serial test - #35418

Closed
robobun wants to merge 7 commits into
mainfrom
farm/53119401/ci-slowest-parallel-misattribution
Closed

robobun wants to merge 7 commits into
mainfrom
farm/53119401/ci-slowest-parallel-misattribution

Conversation

@robobun

@robobun robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

scripts/ci-slowest-tests.ts and scripts/buildkite-slow-tests.js measure a file's wall clock as the gap between its [N/TOTAL] <path> header and the next one. Both required the BuildKite --- group prefix and a literal [90m gray SGR:

/_bk;t=(\d+).*?--- .*?\[90m\[\d+\/\d+\].*?\[0m (.+)/

scripts/runner.node.mjs emits several other headers that this regex treats as invisible, so whichever serial test happens to precede them is charged the entire phase that follows:

header emitted by example
--- Running N parallel-safe tests parallelSafeTests phase start --- Running 444 parallel-safe tests (3-wide)
[N/M] <path> (no --- ) concurrent runTest dispatch [258/829] test/js/.../test-foo.ts
--- napi prebuild: ... maybePrebuildNapi --- napi prebuild: 3 addon(s), 23.9s
--- [A-B/M] K files in parallel runParallelBucket (#36175) --- [52-257/829] 206 files in parallel (3×)
[N/M] <path> (X.XXs) (no --- ) runParallelBucket per-file summary [52/829] test/bake/... (0.00s)
--- [N/M] <path> - <error> retry/error label (yellow/red) --- [16/829] test/... - code 1

Observed on build #86086, :windows: 2019 x64 shard 0:

_bk;t=1785476068608 --- [257/829] test/js/bun/test/fake-timers/sinonjs/issue-2086.test.ts
_bk;t=1785476068661 Ran 0 tests across 1 file. [41.00ms]
_bk;t=1785476068665 --- Running 444 parallel-safe tests (3-wide)          <- not matched
_bk;t=1785476068666 [258/829] test/js/bun/test/parallel/...               <- not matched
...
_bk;t=1785476091162 --- [702/829] vendor/elysia/package.json              <- next match

parseLog reported issue-2086.test.ts at 22554 ms; bun's own summary says 41 ms. Same log, one shard earlier: test/regression/issue/24850.test.ts at [51/829] was charged ~60 s for the napi prebuild and parallel-bucket phase that followed it; bun's own summary says 44 ms.

scripts/update-test-durations.mjs already handled the parallel-safe phase but not the napi prebuild / [A-B/M] headers, so it had the same misattribution for the test preceding the bucket (mitigated by median-of-5-builds).

Fix

parseLog in both slow-test scripts, and the boundary check in update-test-durations.mjs:

  • the runner's non-[N/M] phase headers (napi prebuild:, [A-B/M] K files in parallel, Running N parallel-safe, End, Summary, Received ... exiting) close the open span. This is an allowlist rather than "any --- " because pipeTestStdout's sanitiser is not airtight: on build #86086 a bun patch diff (--- a/index.js) and test/docker/index.ts coordinator output (--- ps ---) both reached the log verbatim and would otherwise truncate the enclosing test's span.
  • [N/M] <path> matches with or without the --- prefix
  • [N/M] <path> (X.XXs) bucket summary lines use the inline timing directly
  • concurrent-dispatch [N/M] <path> spans are clamped to 500 ms (inter-dispatch deltas of an N-wide pool, not wall clock)
  • retry/error labels (<path> - code 1) are boundaries that do not open a new span, so the 5-15 s retry backoff lands on neither attempt
  • a truncated log (job killed mid-run, no --- End) charges the still-open span to the last timestamp seen instead of dropping it

ci-slowest-tests.ts and update-test-durations.mjs export parseLog behind an entrypoint guard so test/internal/ci-slowest-tests.test.ts can exercise all three parsers against the same header fixtures.

Verification

test/internal/ci-slowest-tests.test.ts covers each header form (including the --- a/<file> / --- ps --- false positives). Against real build #86086 logs:

file parseLog before parseLog after bun's own timer
test/js/bun/test/fake-timers/sinonjs/issue-2086.test.ts (windows 2019 x64) 22554 ms 57 ms 41 ms
test/regression/issue/24850.test.ts (windows 2019 x64) 60575 ms 61 ms 44 ms
test/regression/issue/28632.test.ts (debian 13 aarch64) 28996 ms 28 ms 24 ms
test/cli/install/bun-patch.test.ts (debian 13 x64-asan) (n/a) 11643 ms 11.61 s
unique files found (windows 2019 x64 shard 0) 63 829 829

End-to-end across all 162 test-bun jobs in #86086, issue-2086.test.ts is now at rank 5212 (0.06 s); top 5 are genuinely slow files (compile-asset-bunfs 452 s, bundler_compile 121 s, expo 113 s, v8 109 s, serve-error-handler-stream 103 s).


[stamp-90s] gate passed · iteration 6 · 4 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/internal/ci-slowest-tests.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/internal/ci-slowest-tests.test.ts
bun test v1.4.0 (94fadaec1)

test/internal/ci-slowest-tests.test.ts:
(pass) scripts/ci-slowest-tests.ts parseLog > does not charge the parallel-safe phase to the last serial test [26.97ms]
(pass) scripts/ci-slowest-tests.ts parseLog > sums retry attempts and normalizes Windows path separators [6.45ms]
(pass) scripts/ci-slowest-tests.ts parseLog > closes the last serial test at the parallel-phase group header when no parallel headers follow [12.31ms]
(pass) scripts/ci-slowest-tests.ts parseLog > does not charge the parallel-bucket phase to the preceding serial test [11.90ms]
(pass) scripts/ci-slowest-tests.ts parseLog > ignores stray `--- ` lines that are test output, not group headers [7.46ms]
(pass) phase-header boundary > true --- napi prebuild: 3 addon(s), 23.9s [1.13ms]
(pass) phase-header boundary > true --- [52-257/829] 206 files in parallel (3×) [0.34ms]
(pass) phase-header boundary > true --- Running 444 parallel-safe tests (3-wide) [0.21ms]
(pass) phase-header boundary > true --- End [0.20ms]
(pass) phase-header boundary > true --- Summary [0.20ms]
(pass) phase-header boundary > true --- Received SIGTERM, exiting... [0.23ms]
(pass) phase-header boundary > false --- a/index.js [0.20ms]
(pass) phase-header boundary > false --- ps --- [0.20ms]
(pass) phase-header boundary > false --- logs --- [0.21ms]
(pass) phase-header boundary > false ------ [0.22ms]
(pass) phase-header boundary > false ---  [0.25ms]
(pass) phase-header boundary > false --- [52/829] test/a.test.ts [0.22ms]
(pass) scripts/update-test-durations.mjs parseLog > does not charge napi prebuild or the parallel-bucket phase to the preceding serial test [17.12ms]
(pass) scripts/buildkite-slow-tests.js does not fold batch phases into the preceding serial test [1246.57ms]

 19 pass
 0 fail
 32 expect() calls
Ran 19 tests across 1 file. [3.68s]
Exit: 0
diff hotspot
scripts/buildkite-slow-tests.js        |  92 ++++++----
 scripts/ci-slowest-tests.ts            | 299 ++++++++++++++++++++-------------
 scripts/update-test-durations.mjs      | 117 +++++++------
 test/internal/ci-slowest-tests.test.ts | 247 +++++++++++++++++++++++++++
 4 files changed, 551 insertions(+), 204 deletions(-)

gate history · 5 passed · 0 rejected · iteration 6

evidence per changed file
file                                    reads  edits  tests
scripts/buildkite-slow-tests.js             1      3      0
scripts/ci-slowest-tests.ts                 3      7      0
scripts/update-test-durations.mjs           1      2      0
test/internal/ci-slowest-tests.test.ts      1      3      0

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR updates Buildkite test-log parsing for ANSI output, retries, timestamps, serial phases, and parallel summaries. It exports parseLog, preserves the CLI workflow, broadens duration-span termination, and adds parser and subprocess coverage.

Changes

Buildkite timing analysis

Layer / File(s) Summary
Log parsing and phase timing
scripts/ci-slowest-tests.ts, scripts/buildkite-slow-tests.js, scripts/update-test-durations.mjs
The parsers normalize logs, recognize test and phase headers, aggregate retry attempts, process parallel summaries, and close timing spans at group boundaries.
Parser export and CLI workflow
scripts/ci-slowest-tests.ts
parseLog is exported. The CLI keeps build discovery, log retrieval, aggregation, filtering, sorting, and text or JSON output.
Parser and subprocess validation
test/internal/ci-slowest-tests.test.ts
Tests cover ANSI and timestamp formatting, retries, Windows paths, serial and parallel boundaries, inline durations, filtering, and subprocess behavior.

Possibly related PRs

  • oven-sh/bun#33622: Both changes modify Buildkite test-duration parsing and scripts/update-test-durations.mjs.
  • oven-sh/bun#36175: Both changes handle parallel test buckets, phase boundaries, and per-file timing.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing slow-test parsers from charging batch phases to the preceding serial test.
Description check ✅ Passed The description explains the problem, the implementation, and verification results with detailed test evidence, despite using equivalent headings instead of the template headings.

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

@robobun

robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:14 AM PT - Jul 31st, 2026

✅ @robobun, your commit 94fadaec17df2799a699b1b0eb0fbf99fe984444 passed in Build #86304! 🎉


🧪   To try this PR locally:

bunx bun-pr 35418

That installs a local version of the PR into your bun-35418 executable, so you can run:

bun-35418 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — corrects a real measurement bug in a dev-tooling script, with tests.

What was reviewed:

  • Verified against scripts/runner.node.mjs: concurrent phase uses console.log(grouptitle) (no --- ), serial uses startGroup, and the Running N parallel-safe / End group headers and [attempt #N] / - <error> retry labels all exist as the new regexes expect.
  • Parser logic mirrors the existing implementation in scripts/update-test-durations.mjs (same header regex, same 500 ms concurrent clamp, same boundary-close pattern).
  • import.meta.main guard correctly isolates the fetch/aggregate path so importing parseLog from the test has no side effects; test/internal/ is the established home for tooling tests.
Extended reasoning...

Overview

Fixes scripts/ci-slowest-tests.ts, an offline BuildKite-log analysis script, so it stops mis-attributing the entire parallel-safe test phase's wall clock to the last serial test on each shard. The change rewrites parseLog to (a) match [N/M] path headers with or without the --- group prefix, (b) close open spans at the --- Running N parallel-safe and --- End group boundaries, (c) clamp concurrent-phase spans to 500 ms since header gaps there are dispatch deltas not per-file wall clock, and (d) treat retry/error labels as delimiters so retry backoff isn't charged to either attempt. The rest of the file is moved verbatim under an import.meta.main guard so parseLog can be exported and unit-tested. A new test/internal/ci-slowest-tests.test.ts covers the three shapes: serial→parallel transition, retry attempts with Windows path separators, and shards with empty/absent parallel phases.

Security risks

None. This is a local developer script that parses CI log text and prints a table; it does not ship in the runtime, touches no auth/crypto/permissions, and the only network access (BuildKite log fetch) is unchanged and gated behind import.meta.main.

Level of scrutiny

Low. This is internal tooling in scripts/, not runtime or user-facing code. The worst failure mode is a wrong slow-test report. I cross-checked every claim in the PR description against the actual producer (scripts/runner.node.mjs lines 617–645, 882, 937) and the sibling parser it aligns with (scripts/update-test-durations.mjs lines 66–101) — the header formats, ANSI wrapping, retry label shape, and group-boundary strings all match exactly. The isPath extension regex correctly rejects path - code 1 retry labels while accepting .ts/.tsx/.js/.jsx/.cjs/.mjs/.cts/.mts/.json.

Other factors

  • The new tests assert exact millisecond values derived from the synthetic fixtures (not loose bounds), cover CRLF (\r\r\n) and LF line endings, Windows backslash normalization, and the [attempt #N] suffix stripping.
  • The bulk of the diff is indentation from wrapping existing code in if (import.meta.main) { ... }; the fetch/aggregate/output logic is byte-identical.
  • test/internal/ already hosts similar tooling tests (expected-durations.test.ts, powershell-escape.test.ts), so placement follows convention.
  • No prior human or bot review comments to address; CI build #79541 is in progress but this change requires no native rebuild.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — internal CI tooling fix with a clear root cause and matching tests.

What was reviewed:

  • Verified against scripts/runner.node.mjs that serial headers go through startGroup (--- [N/M]) while parallel-safe headers are bare console.log — the old regex's mandatory --- did miss them.
  • Confirmed --- Running N parallel-safe and --- End are the actual group boundaries emitted by startGroup in utils.mjs, so the new close points are correct.
  • Checked the retry-label path: [N/M] path - <error> fails the trailing-extension isPath check and correctly acts as a delimiter without accruing backoff time.
  • import.meta.main guard keeps the ci:slowest package script working; only caller is the new test.
Extended reasoning...

Overview

This PR fixes scripts/ci-slowest-tests.ts, an internal diagnostic script that scrapes BuildKite logs to rank test files by wall-clock time. The old header regex required the --- group prefix, which runner.node.mjs only emits for the serial phase (via startGroup); the parallel-safe phase prints bare [N/M] path lines. As a result the last serial file on every shard was charged the entire parallel phase. The fix strips ANSI, matches headers with or without --- , closes open spans at the --- Running N parallel-safe / --- End group boundaries, clamps concurrent-phase spans to 500 ms (they're inter-dispatch deltas, not per-file wall clock), and treats retry/error labels as delimiters so the random retry backoff isn't attributed to either attempt. parseLog is now exported behind import.meta.main and covered by three unit tests in test/internal/ci-slowest-tests.test.ts.

Security risks

None. This is a developer-local diagnostic script that reads BuildKite logs and prints a table. It does not run in CI's test path, does not touch runtime code, and processes only trusted CI log output. No new external inputs or network surfaces were introduced.

Level of scrutiny

Low. scripts/ci-slowest-tests.ts is dev tooling — it feeds the ci:slowest package script and the slowest-tests skill, not the shipped binary. Worst-case failure is a wrong slow-test report. I cross-checked every log-format assumption against scripts/runner.node.mjs and scripts/utils.mjs: startGroup prints --- ${title} on BuildKite, concurrent runTest uses plain console.log(grouptitle), the retry label is [N/M] title - <error>, and the terminal groups are literally Running N parallel-safe tests (K-wide) and End. All match the new regexes.

Other factors

  • The bulk of the diff is indentation from wrapping the CLI body in if (import.meta.main); the logic change is confined to parseLog.
  • Tests exercise the exact failure modes described (parallel-phase absorption, retry backoff exclusion, Windows path normalization, empty-parallel and no-parallel shard termination) with strong toBe assertions on millisecond values, and were shown passing under bun bd test in the PR evidence.
  • Grep confirms no other importer of this file besides the new test, so exporting parseLog and adding the import.meta.main guard does not affect the bun run ci:slowest entry point.
  • The PR description includes before/after numbers from a real build (#79247) demonstrating the fix (79 524 ms → 4 838 ms for the misattributed file, entry count 1113 → 2732).

@robobun

robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status: test/internal/ci-slowest-tests.test.ts passes on every lane in every build (#79541, #79567, #86287, #86304).

Remaining red lanes are unrelated to this diff (which only touches scripts/ci-slowest-tests.ts, scripts/buildkite-slow-tests.js, scripts/update-test-durations.mjs, and the new internal test):

  • #86304: every failure is marked flaky (passed on retry or passed alone outside the parallel batch); none are new
  • #86287: filesystem_router.test.ts segfault on debian 13 aarch64 (native runtime crash)
  • earlier builds: npm registry 522/503 and api.github.com 504 on install tests

All four review threads have been addressed (three applied in 96e8f93, one withdrawn). Ready for review.

robobun and others added 4 commits July 31, 2026 10:00
…t serial test

parseLog measured each file as (next-header - this-header) but only matched
the `--- [N/M] path` form. runner.node.mjs prints parallel-safe headers
without the `--- ` prefix, so the last serial file on every shard absorbed
the entire parallel phase (79.5s reported for a 4.8s test on build 79247).

Match the header with or without `--- `, close the open span at the
`--- Running N parallel-safe tests` / `--- End` group boundaries, clamp
concurrent-phase spans (they are inter-dispatch deltas, not wall clock),
and drop the retry backoff between a failure label and the next attempt.
This matches the parseLog already used by scripts/update-test-durations.mjs.

parseLog is now exported behind an import.meta.main guard so the new
test/internal/ci-slowest-tests.test.ts can exercise it directly.
…ibling parsers

Since #36175, runner.node.mjs also emits:
  --- napi prebuild: N addon(s), Xs
  --- [A-B/M] K files in parallel (Nx)
  [N/M] <path> (X.XXs)               (bucket per-file summary)

none of which match '^--- [N/M] <path>'. Close the open span on any
'--- ' group header so the test preceding the bucket is not charged the
whole bucket run, and read the inline '(X.XXs)' timing from bucket
summary lines.

buildkite-slow-tests.js and update-test-durations.mjs share the same
header contract and get the same fix.
@robobun
robobun force-pushed the farm/53119401/ci-slowest-parallel-misattribution branch from 118dbdc to 1b8991a Compare July 31, 2026 10:04
@robobun robobun changed the title scripts/ci-slowest-tests: stop charging the parallel phase to the last serial test scripts: stop the slow-test parsers charging batch phases to the preceding serial test Jul 31, 2026
@robobun

robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main and extended to cover the runner output added in #36175 (merged after this PR was opened): the --- napi prebuild: ... and --- [A-B/M] K files in parallel group headers, and the [N/M] <path> (X.XXs) bucket summary lines. The open span now closes on any --- group header, not just the two that existed before.

Applied the same fix to scripts/buildkite-slow-tests.js (same regex, same bug) and to the boundary check in scripts/update-test-durations.mjs. Added a test case for the bucket phase and for buildkite-slow-tests.js.

Verified against build #86086 (see updated PR body).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/buildkite-slow-tests.js`:
- Around line 34-39: Update the non-“--- ” branch in the parser around the
timed-header handling to recognize bare “[N/M] <path>” parallel headers emitted
by runner.node.mjs. Record each as a concurrent span using its path and the
existing inter-dispatch duration logic, clamping that duration to 500 ms, while
preserving the current summary-line parsing and continue behavior.

In `@scripts/ci-slowest-tests.ts`:
- Around line 48-82: Flush the final open test span when log parsing reaches EOF
by tracking the most recent APC timestamp and invoking the parser’s close
operation after the loop in scripts/ci-slowest-tests.ts lines 48-82. Apply the
equivalent last-timestamp tracking and finalization in
scripts/buildkite-slow-tests.js lines 27-55 before building testGroups, and call
emit(lastTs) in scripts/update-test-durations.mjs lines 79-113 before returning
out; preserve existing boundary handling.
- Around line 60-66: Update the timed-summary handling in the slowest-tests
parser to validate the captured name as a test file before adding it to out.
Align this check with the extension validation used by the sibling
update-test-durations parser, and only update out, concurrent, and continue for
valid test-file names; otherwise let the normal parsing path handle the line.

In `@test/internal/ci-slowest-tests.test.ts`:
- Line 34: Update the assertion for test/js/node/test/parallel/p3.js to use
exact equality with 500 via toBe, replacing the weaker upper-bound check while
preserving the existing fixture and test structure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7dc4dc3-a887-4e8d-ae04-43dce061f5ae

📥 Commits

Reviewing files that changed from the base of the PR and between 37a7767 and ca050cc.

📒 Files selected for processing (4)
  • scripts/buildkite-slow-tests.js
  • scripts/ci-slowest-tests.ts
  • scripts/update-test-durations.mjs
  • test/internal/ci-slowest-tests.test.ts

Comment thread scripts/buildkite-slow-tests.js Outdated
Comment thread scripts/ci-slowest-tests.ts
Comment thread scripts/ci-slowest-tests.ts
Comment thread test/internal/ci-slowest-tests.test.ts Outdated
…ary path

- flush the open span at end-of-log in all three parsers so a job killed
  or timed out mid-run still records the last file instead of dropping it
- require a test-file extension in the parallel-bucket summary name,
  matching update-test-durations.mjs
- assert toBe(500) for the clamped parallel-safe span (fixture is
  deterministic) and cover the truncated-log case

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — internal CI-tooling fix with good test coverage.

Checked: the import.meta.main wrap in ci-slowest-tests.ts is a pure indent of the existing CLI body; the broadened --- boundary in update-test-durations.mjs cannot over-close because [N/M] headers hit the hdr branch first and [A-B/M] bucket headers do not match \[\d+\/\d+\]. The subprocess test drains stdout/stderr/exited concurrently and uses tempDir/bunEnv per harness conventions. All CodeRabbit findings were addressed in 96e8f93 or withdrawn.

Extended reasoning...

Overview

This PR fixes three internal BuildKite log parsers (scripts/ci-slowest-tests.ts, scripts/buildkite-slow-tests.js, scripts/update-test-durations.mjs) that were mis-attributing entire batch phases (parallel-safe tests, napi prebuild, parallel buckets) to whichever serial test happened to run immediately before them. The fix: any --- group header now closes the open timing span, [N/M] headers match with or without the --- prefix, parallel-bucket (X.XXs) summary lines use their inline timing, concurrent-dispatch spans are clamped to 500 ms, and retry/error labels close but don't open spans. parseLog is exported behind an import.meta.main guard so test/internal/ci-slowest-tests.test.ts (new) can drive it directly with synthetic fixtures; a subprocess test covers buildkite-slow-tests.js.

Security risks

None. These are developer-run CI analysis scripts that read BuildKite job logs and write reports/JSON. No user-facing surface, no runtime code, no auth/crypto/permissions.

Level of scrutiny

Low. This is internal tooling under scripts/ — the worst failure mode is an inaccurate slow-test report or suboptimal shard bin-packing in expected-durations.json (which update-test-durations.mjs already mitigates via median-of-5-builds and a >1000-paths sanity guard). Nothing here ships in the bun binary or affects user code.

Other factors

  • The bug-hunting system found no issues.
  • All four CodeRabbit findings were resolved: EOF flush of the open span in all three parsers (with a truncated-log fixture), exact toBe(500) clamp assertion, test-file-extension gate on the timed-summary regex; the fourth (record bare parallel-safe headers in buildkite-slow-tests.js) was correctly rebutted and withdrawn — the --- Running N parallel-safe tests header already reaches close(ts), and clamped inter-dispatch deltas would never survive the script's > 1 s filter anyway.
  • The if (import.meta.main) block is a pure re-indent of the existing CLI code — I diffed it visually and the logic is unchanged.
  • The new test file lives alongside existing internal-tooling tests (test/internal/expected-durations.test.ts, etc.), follows harness conventions (tempDir, bunEnv, bunExe, concurrent pipe drain, exit-code asserted last), and asserts exact values on deterministic fixtures.
  • PR body documents verification against real build #86086 logs showing the misattribution fixed (22554 ms → 57 ms, etc.) and file-count going from 63 → 829.

…update-test-durations.mjs

Closing the open span on any '--- ' line also matched test stdout that
slipped past pipeTestStdout's sanitiser: unified-diff '--- a/<file>'
inside bun-patch.test.ts (under-counted by ~33% on build #86086) and
'--- ps ---' / '--- logs ---' from test/docker/index.ts inside
test/package.json (~58%). Limit the boundary to the startGroup titles
runner.node.mjs actually emits and cover the false positives in the
fixture.

Export parseLog from update-test-durations.mjs behind an entrypoint
check so the test can run the same fixture through the parser CI shard
bin-packing consumes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — well-scoped fix to internal CI log parsers with thorough test coverage.

What was reviewed:

  • Verified the isPhaseGroupHeader allowlist against scripts/runner.node.mjs — all six startGroup phase titles (napi prebuild, [A-B/M] bucket, parallel-safe, End, Summary, Received-exiting) are covered; the one uncovered startGroup (<path> - N failing in the parallel batch) only fires after the timed-summary lines when no span is open, so it needs no boundary handling.
  • Checked the import.meta.main / process.argv[1] entrypoint guards and the strict: false + deferred token check in update-test-durations.mjs — importing for tests no longer triggers CLI side-effects or exits.
  • All four CodeRabbit threads are resolved (EOF flush, exact clamp assertion, timed-summary extension check applied; bare-header recording correctly declined for buildkite-slow-tests.js since its > 1 s filter would drop them anyway).
Extended reasoning...

Overview

This PR fixes three internal CI log-parsing scripts (scripts/ci-slowest-tests.ts, scripts/buildkite-slow-tests.js, scripts/update-test-durations.mjs) that measure per-test-file wall-clock time from BuildKite's APC-timestamped log lines. The bug: the old regex only recognized --- [N/M] <path> headers with a literal [90m SGR, so any other phase header (--- Running N parallel-safe tests, --- napi prebuild:, --- [A-B/M] K files in parallel, bare [N/M] concurrent-dispatch lines, retry/error labels) was invisible, and whichever serial test preceded such a phase was charged the entire following batch — e.g. a 41 ms test reported as 22.5 s. The fix strips ANSI upfront, recognizes all header shapes runner.node.mjs emits, uses an allowlist of phase-header titles (rather than any --- ) to avoid false positives from unified-diff --- a/<file> lines in test stdout, records inline (X.XXs) timings from parallel-bucket summaries, clamps concurrent inter-dispatch deltas to 500 ms, and flushes the open span at EOF for truncated logs. Both parseLog functions are now exported behind entrypoint guards so a new test/internal/ci-slowest-tests.test.ts can exercise all three parsers against shared fixtures.

Security risks

None. These are developer-run and scheduled CI-tooling scripts that read BuildKite logs and print/write timing tables. No runtime code, no user input surface, no auth/crypto/permissions.

Level of scrutiny

Low. This is internal tooling, not shipped in the bun binary. The worst-case failure mode is a mis-ranked slow-test report or a slightly skewed expected-durations.json (which is median-of-5-builds and guarded by a paths.size < 1000 sanity check). I cross-checked the phase-header allowlist against the actual startGroup(...) calls in scripts/runner.node.mjs at HEAD and every phase title is covered; the one startGroup not in the allowlist (<path> - N failing in the parallel batch, line 1102) only prints after the timed-summary lines have already closed the span.

Other factors

  • Comprehensive test coverage: the new test file exercises the parallel-safe phase, parallel-bucket phase with inline timings, retry/error labels with backoff, Windows path separators, truncated logs, and stray --- false positives (--- a/index.js, --- ps ---). It also runs buildkite-slow-tests.js end-to-end as a subprocess.
  • All four CodeRabbit review threads are resolved (three applied, one correctly withdrawn with reasoning).
  • CI: the new test passes on every lane; remaining red is an unrelated native segfault.
  • The import.meta.main refactor and strict: false in parseArgs are the minimal changes needed to make the parsers importable without running the CLI or exiting on missing BUILDKITE_API_TOKEN.

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Re-checked this branch's parseLog against a current build, #99368 (merged-PR build from today, 158 test-bun jobs), since the runner now emits the [A-B/M] K files in parallel bucket header on every platform.

On main, bun run ci:slowest 99368 20 ranks these at the top; each one is a serial file that happened to run right before the bucket on one shard (the raw log shows e.g. complex-operations.test.ts finishing in ~65 ms, followed by --- [25-107/298] 83 files in parallel):

file main this branch
test/js/valkey/integration/complex-operations.test.ts 600.89 s 0.29 s
test/js/valkey/unit/list-operations.test.ts 600.20 s 0.29 s
test/regression/issue/26063.test.ts 421.60 s 0.32 s
test/js/web/websocket/websocket-proxy.test.ts 382.02 s 2.62 s
test/js/valkey/valkey-tls-verify.test.ts 98.11 s 0.28 s

With this branch the top of the list is test/js/bun/cron/in-process-cron.test.ts (146 s, alpine x64) and test/js/bun/spawn/spawn.test.ts (132 s, x64-asan). The cron file reads 154 s on main because one shard retried it and main's parser also charges the retry backoff between the - code 1 label and [attempt #2], which this branch stops doing.

robobun added a commit that referenced this pull request Aug 23, 2026
…) into farm/2df9101c/ci-slowest-docker-wait

main deleted scripts/buildkite-slow-tests.js (#39581); drop #35418's hunk for
it and the test that spawned it.
robobun added a commit that referenced this pull request Aug 31, 2026
Both slow-test parsers (scripts/ci-slowest-tests.ts and
scripts/update-test-durations.mjs) measured a file as the gap between
its `[N/M] <path>` header and the next header they recognized. Every
other header runner.node.mjs emits (`--- napi prebuild:`, the
`--- [A-B/M] K files in parallel` bucket, `--- Running N parallel-safe`,
`--- End`, `--- Summary`, `--- Received <signal>, exiting`) was
invisible, so the serial test that happened to run before one of them
was charged the whole phase that followed.

- Those phase headers close the open span. The check is an allowlist of
  the titles the runner emits, not any `--- ` line, because streamed
  test output can still contain `--- a/<file>` or `--- ps ---`.
- `[N/M] <path>` matches with or without the `--- ` prefix. Bucket
  summary lines `[N/M] <path> (X.XXs)` use the inline timing.
  Concurrent dispatch spans are clamped to 500 ms.
- Retry/error labels (`<path> - code 1`) are a boundary that opens no
  span, so the retry backoff lands on neither attempt.
- A truncated log (no `--- End`) charges the open span to the last
  timestamp seen instead of dropping it.

Both scripts export parseLog behind an entrypoint guard, and
test/internal/ci-slowest-tests.test.ts exercises them against the same
header fixtures. scripts/buildkite-slow-tests.js from #35418 is gone
from main (#39581) and is not restored.

Regenerated test/expected-durations.json with the combined parser from
builds 108915, 108911, 108910, 108909, 108906.
@robobun

robobun commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: folded into #41070 together with the windows-aarch64 duration column and a table regenerated with the combined parser. The buildkite-slow-tests.js part is dropped because main deleted that script in #39581.

@robobun robobun closed this Aug 31, 2026
Jarred-Sumner pushed a commit that referenced this pull request Sep 1, 2026
…sers, regenerate the table (#41070)

### Problem
- `windows-aarch64-11-test-bun` packs its 8 shards with the timings of
the windows x64 lane. Its serial files run about 2x slower and not
uniformly (`bundler/bundler_compile.test.ts` is 222s there, 48s in the
x64 column). One shard carries 922s of work against a 610s mean. This
lane finished last in 46 of the 47 completed builds since #40993 merged,
so it sets the build time: the median build went from 1342s to 1386s
while the linux lanes got faster (x64 glibc slowest shard 233s to 178s).
- The slow-test log parsers (`scripts/update-test-durations.mjs`,
`scripts/ci-slowest-tests.ts`) only recognize `[N/M] <path>` headers.
Every other header the runner emits (`--- napi prebuild:`, the `---
[A-B/M] K files in parallel` bucket, `--- Running N parallel-safe`) is
invisible, so the serial file that runs right before one is charged the
whole phase that follows. In source build 108642 this hit 19 of 20 linux
shards and 8 of 8 windows shards
(`js/valkey/unit/basic-operations.test.ts` recorded as 111.6s on
windows, real 0.1s). 11 of the phantom entries are 10s or more in the
default column, so `--skip-slower-than=10000` dropped those files from
the darwin PR lanes.

### Fix
- Add a `windows-aarch64` column, measured from
`windows-aarch64-11-test-bun`. `runner.node.mjs` picks it for the
windows-aarch64 step and falls back to `windows`, then the other
columns, for a file that lacks one.
- Fold in the parser fixes from #35418 (this PR supersedes it): the
runner's phase headers close the open span (an allowlist of the titles
the runner emits, since test output can contain `--- a/<file>`), bucket
summary lines use their inline `(X.XXs)` timing, retry labels open no
span, and a truncated log keeps its last open span. Both scripts export
`parseLog` behind an entrypoint guard, and
`test/internal/ci-slowest-tests.test.ts` runs them against the same
fixtures. `scripts/buildkite-slow-tests.js` from #35418 was deleted from
main in #39581 and is not restored.
- Regenerate `test/expected-durations.json` with the combined parser
from builds 108915, 108911, 108910, 108909, 108906 (`node
scripts/update-test-durations.mjs --builds 5`, 5906 entries).
- Verified: `bun bd test test/internal/ci-slowest-tests.test.ts` (18
pass). Replaying the LPT packing against per-file costs measured on
three other windows-aarch64 runs (builds 108785, 108786, 108801) gives a
slowest shard of 673s with the new column, against 922s with the table
on main (610s is the floor). Column selection checked for every step
key.

### Background
- `runner.node.mjs` bin-packs test files across `--max-shards` with LPT
(longest file first into the least loaded bin) using
`test/expected-durations.json`. Every shard computes the same assignment
from the same input, so the shards need no coordination.
- The table has one column per lane with distinct timing: `default`
(linux x64 debian), `asan`, `musl`, `windows`. Every other lane uses the
nearest column (darwin uses `default`).
- Per-file cost comes from the `_bk;t=<ms>` timestamps Buildkite
prefixes to each log line: the gap between one `[N/M] <path>` group
header and the next header. The parallel bucket runs one `bun test
--parallel` for many files and prints `[N/M] <path> (X.XXs)` per file
afterwards.
- The untiered darwin PR lanes pass `--skip-slower-than=10000`, which
drops a file when its column cost is 10s or more.
- `.buildkite/update-test-durations.yml` regenerates the file on a
schedule and uploads it as an artifact. A human commits it.

<details><summary>Notes</summary>

Measurement: Buildkite REST job timings for 95 builds created after the
merge of #40993 (09:17 UTC, 2026-08-31). 64 builds include the new table
(GitHub compare status `ahead`), 31 PR builds still ran on the old one.
Same fleet, same hours. Median of the slowest shard per lane, old table
to new table:

| lane | old | new |
|---|---|---|
| linux-x64-debian-13 | 233s | 178s |
| linux-x64-asan | 437s | 387s |
| linux-aarch64-debian-13 | 255s | 230s |
| linux-x64-ubuntu-2504 | 232s | 178s |
| musl (both arches) | 271-282s | 281-294s |
| windows-x64-2019 | 491s | 468s |
| windows-aarch64-11 | 829s | 887s |
| darwin-aarch64-any | 534s | 581s (2 shards, balanced per shard: 506s
and 518s) |
| build start to last test finished | 1342s | 1386s |

windows-aarch64 per-shard medians with the old table: `456 523 594 572
477 375 823 526` (shard 6 slowest in 177 of 188 builds). With the table
from #40993: `447 404 575 892 521 651 416 453` (shard 3 slowest in 49 of
49). The runner estimated that shard at 429s.

Largest windows-aarch64 costs in the new column (s): bundler_compile
219, serve-body-leak 154, expo 114, v8 97, test-fs-read-stream-pos 90,
napi 85.

Bucket header bug, source build 108642, default column (parsed vs real):
websocket-proxy-tunnel-client-leak 17.6 vs 0.0,
valkey/integration/complex-operations 32.8 vs 0.1,
regression/issue/24850 15.7 vs 0.1, sql-mysql-bigint-out-of-range 24.4
vs 0.3. Windows column: valkey/unit/basic-operations 122.5 vs 0.1,
regression/issue/28632 76.3 vs 0.1. In the logs checked, the `--- napi
prebuild:` header carries the same timestamp as the next file header, so
it misattributes nothing today; the allowlist still covers it.

From #35418, against build 86086 logs:
`test/js/bun/test/fake-timers/sinonjs/issue-2086.test.ts` parsed at
22554 ms before, 57 ms after (bun's own timer: 41 ms).
`test/regression/issue/24850.test.ts` 60575 ms before, 61 ms after.
Unique files found in one windows shard: 63 before, 829 after (the old
`ci-slowest-tests.ts` regex required a literal `[90m` SGR and the `--- `
prefix).

Side effect: `scripts/update-parallel-allowlist.mjs` takes the slowest
column per file. With the new column, 10 files that are under 15s on
every other lane are over 15s on windows-aarch64 and will leave the
parallel allowlist at its next regeneration (for example
`js/bun/spawn/spawn-noread-leak.test.ts` at 38.7s). That is the stated
intent of that check.

The regenerated default column has 44 entries at 10s or more against 55
on main. Files that the darwin PR lanes skipped because of phantom
entries and now run again: websocket-proxy.test.ts,
websocket-proxy-tunnel-client-leak, websocket-proxy-tunnel-upgrade-leak,
regression/issue/24850, regression/issue/patch-bounds-check,
cli/install/bun-install-proxy.

</details>

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 2 · 6 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/internal/ci-slowest-tests.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/internal/ci-slowest-tests.test.ts
bun test v1.4.1 (a6c4cc2)

test/internal/ci-slowest-tests.test.ts:
(pass) scripts/ci-slowest-tests.ts parseLog > does not charge the parallel-safe phase to the last serial test [28.64ms]
(pass) scripts/ci-slowest-tests.ts parseLog > sums retry attempts and normalizes Windows path separators [6.28ms]
(pass) scripts/ci-slowest-tests.ts parseLog > closes the last serial test at the parallel-phase group header when no parallel headers follow [11.47ms]
(pass) scripts/ci-slowest-tests.ts parseLog > does not charge the parallel-bucket phase to the preceding serial test [12.70ms]
(pass) scripts/ci-slowest-tests.ts parseLog > ignores stray `--- ` lines that are test output, not group headers [7.17ms]
(pass) phase-header boundary > true --- napi prebuild: 3 addon(s), 23.9s [1.37ms]
(pass) phase-header boundary > true --- [52-257/829] 206 files in parallel (3×) [0.32ms]
(pass) phase-header boundary > true --- Running 444 parallel-safe tests (3-wide) [0.19ms]
(pass) phase-header boundary > true --- End [0.18ms]
(pass) phase-header boundary > true --- Summary [0.18ms]
(pass) phase-header boundary > true --- Received SIGTERM, exiting... [0.18ms]
(pass) phase-header boundary > false --- a/index.js [0.20ms]
(pass) phase-header boundary > false --- ps --- [0.17ms]
(pass) phase-header boundary > false --- logs --- [0.17ms]
(pass) phase-header boundary > false ------ [0.18ms]
(pass) phase-header boundary > false ---  [0.27ms]
(pass) phase-header boundary > false --- [52/829] test/a.test.ts [0.19ms]
(pass) scripts/update-test-durations.mjs parseLog > does not charge napi prebuild or the parallel-bucket phase to the preceding serial test [17.04ms]

 18 pass
 0 fail
 24 expect() calls
Ran 18 tests across 1 file. [2.22s]
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
scripts/ci-log-phase.mjs               |    32 +
 scripts/ci-slowest-tests.ts            |   282 +-
 scripts/runner.node.mjs                |    18 +-
 scripts/update-test-durations.mjs      |   116 +-
 test/expected-durations.json           | 49849 +++++++++++++++++--------------
 test/internal/ci-slowest-tests.test.ts |   196 +
 6 files changed, 28355 insertions(+), 22138 deletions(-)
```

</details>

**gate history** · 3 passed · 0 rejected · iteration 2

<details><summary>evidence per changed file</summary>

```
file                                    reads  edits  tests
scripts/ci-log-phase.mjs                    0      1      0
scripts/ci-slowest-tests.ts                 1      1      0
scripts/runner.node.mjs                     4      1      0
scripts/update-test-durations.mjs           4      6      0
test/expected-durations.json                0      0      0
test/internal/ci-slowest-tests.test.ts      3      4      0
```

</details>

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant