Skip to content

test: make request-method-getter.test.ts faster and stricter - #40890

Open
robobun wants to merge 1 commit into
mainfrom
robobun/606c01dc/request-method-getter-test-speed
Open

robobun wants to merge 1 commit into
mainfrom
robobun/606c01dc/request-method-getter-test-speed

Conversation

@robobun

@robobun robobun commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • test/js/web/request/request-method-getter.test.ts takes 19.3s on the debian 13 x64-asan lane (build #108217) and under 3s on release lanes. A local debug ASAN build takes 124s, and every test exceeds the 5s timeout.
  • Each row runs 512K clone().method reads and 128K new Request().method reads against a fixed budget of 512 new JSStrings. A few thousand reads detect a per-call allocation.

Fix

  • Size the loops by the assertion: 1024 reads under ASAN or debug, 8192 in release. The budget is iterations / 8, so a per-call allocation overshoots it by 8x.
  • Keep every returned string alive and run Bun.gc(true) before both heapStats() readings. A leaked string stays live until the final reading, so the count does not depend on when the GC runs.
  • Assert the value ("GET" or "POST"), that every clone reports its source's method, and that method: "post" normalizes to "POST" on the same cached string. The failure message reports the measured growth.
  • Verified: debug ASAN file time 124.4s before, 4.9s after, stable over 6 debug and 10 release runs. On the debian 13 x64-asan lane the file takes 362ms (build 108294). Reading .url instead of .method fails all 8 cases with growth 1022 of 1024 (debug) and 8190 of 8192 (release).

Background

  • heapStats().objectTypeCounts.string (bun:jsc) counts JSString cells on the heap without a GC, so dead strings count until a collection sweeps them.
  • request.method returns one cell from the common strings table (Bun__HTTPMethod__toJS) on every call. Make request.method getter not allocate memory #18961 made it so, and this test guards that.
Notes
  • Release build (USE_SYSTEM_BUN=1): 486ms before, 147ms after.
  • Noise in the passing case: growth between -3 and +2 strings, in both builds, for loop sizes from 256 to 16384.
  • Without retention, a full GC before the final reading hides a per-call allocation (growth -1 when reading .url). Retention is what makes the final GC safe. Without the final GC the old test counted dead strings, which works until an Eden collection runs inside the loop.
  • The tests stay serial. The string count is process wide, and the test bodies are synchronous, so test.concurrent would not overlap them anyway.
  • make more tests faster #33704 (open, 54 files, conflicting) scales this file's loops down 8x under ASAN and keeps the constant 512 budget. This PR replaces that change for this file.
  • Observed but not touched: new Request(url, { method: "Post" }) and { method: "BREW" } both return "GET" in Bun. Node returns "POST" and "BREW". The new row uses the all-lowercase form, which Bun accepts.

[stamp-90s] gate passed · iteration 0 · 1 files touched

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

Debug/ASAN (expected pass):
$ bun bd test 'test/js/web/request/request-method-getter.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/web/request/request-method-getter.test.ts
bun test v1.4.1 (d578a8c70)

test/js/web/request/request-method-getter.test.ts:
(pass) new Request(url, ...[]).clone().method is GET and does not allocate a JSString per call [325.99ms]
(pass) new Request(url, ...[{"method":"GET"}]).clone().method is GET and does not allocate a JSString per call [288.28ms]
(pass) new Request(url, ...[{"method":"POST"}]).clone().method is POST and does not allocate a JSString per call [304.56ms]
(pass) new Request(url, ...[{"method":"post"}]).clone().method is POST and does not allocate a JSString per call [280.68ms]
(pass) new Request(url, ...[]).method is GET and does not allocate a JSString per call [338.96ms]
(pass) new Request(url, ...[{"method":"GET"}]).method is GET and does not allocate a JSString per call [414.88ms]
(pass) new Request(url, ...[{"method":"POST"}]).method is POST and does not allocate a JSString per call [405.94ms]
(pass) new Request(url, ...[{"method":"post"}]).method is POST and does not allocate a JSString per call [408.47ms]

 8 pass
 0 fail
 20 expect() calls
Ran 8 tests across 1 file. [4.75s]
Exit: 0
diff hotspot
test/js/web/request/request-method-getter.test.ts | 89 +++++++++++------------
 1 file changed, 44 insertions(+), 45 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                               reads  edits  tests
test/js/web/request/request-method-getter.test.ts      2      5      0

Size the loops by what the string-count assertion needs: 1024 reads under
ASAN or debug, 8192 in release, instead of 512K clones and 128K
constructions per row. The budget is a fraction of the iteration count,
so a getter that allocates one JSString per call overshoots it by 8x.

Keep every returned string alive and run a full GC before both heap
readings, so the count does not depend on when the GC runs. Also assert
the returned value, that every clone reports the same method as its
source, and that a lowercase `method: "post"` normalizes to "POST" on
the same cached string.
@robobun

robobun commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. CI is done for 29e82c2; the diff is green on every lane.

Reproduced the slow run with a local debug ASAN build: bun bd test test/js/web/request/request-method-getter.test.ts took 124.4s, and each of the 6 tests exceeded the 5s timeout. With this change the same command takes 4.9s, 8 tests, stable over 6 runs. On the debian 13 x64-asan lane in build 108294 the file takes 362ms (19.3s before).

Detector check: a copy of the new file that reads .url (a new JSString per call) instead of .method fails all 8 cases, with growth 1022 of 1024 reads under the debug build and 8190 of 8192 under the release build.

Build 108294 has one red job: test/js/web/url/url.test.ts on darwin x64 (TypeError: Invalid URL in the Unicode 16 IDNA test). That test fails on main too and is not related to this change. The other reported failures passed on retry.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3565260c-de1e-4bd7-a57e-ba8a1d9b7c6c

📥 Commits

Reviewing files that changed from the base of the PR and between 1ab272b and 29e82c2.

📒 Files selected for processing (1)
  • test/js/web/request/request-method-getter.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.


Walkthrough

The request method tests now parameterize method inputs, verify uppercase normalization for cloned and newly constructed requests, retain getter results during allocation checks, and adapt thresholds for ASAN and debug builds.

Changes

Request method getter validation

Layer / File(s) Summary
Method normalization and allocation checks
test/js/web/request/request-method-getter.test.ts
Parameterized tests cover default, GET, POST, and lowercase post inputs. They verify normalized values, returned-value identity, and bounded live JSString growth for cloned and newly constructed requests.

Merge Risk: ⚪ Minimal · up to 29e82

This test-only change makes the request-method getter checks faster and stricter while preserving the expected GET/POST behavior; no actionable merge-blocking risk remains.

🚥 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.
Description check ✅ Passed The description clearly explains the problem, the test changes, and verification results. It provides equivalent information for both required template sections, although it uses different headings.
Title check ✅ Passed The title is concise and accurately describes the main change: making the request method getter test faster and stricter.

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

@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 — test-only refactor that makes the leak guard faster and deterministic.

What was reviewed:

  • Retention of every returned string plus Bun.gc(true) before both heapStats() reads makes the growth measurement independent of mid-loop GC timing; a per-call allocation overshoots the iterations / 8 budget by 8x.
  • isASAN || isDebug branching on iteration count follows the repo's leak-test convention; no per-test timeouts added.
  • The http://localhost:3000/ literal is only a Request constructor argument — no socket is bound, so the port: 0 rule does not apply.
  • New value assertions (distinct equals [method]) and the lowercase-normalization row strengthen coverage without changing what the file guards.
Extended reasoning...

Overview

This PR refactors a single test file, test/js/web/request/request-method-getter.test.ts, which guards against Request.prototype.method allocating a fresh JSString on every read (the fix landed in #18961; the getter should return a cached common-strings-table cell). The old test ran 512K/128K iterations and asserted heapStats().objectTypeCounts.string grew by fewer than a fixed 512 — a bound that depended on GC not sweeping mid-loop, and a workload that took ~124s under debug ASAN. The new version drops to 8192 iterations (1024 under isASAN || isDebug), pushes every returned string into an array so any per-call allocation is pinned live across both measurements, runs Bun.gc(true) before each heapStats() reading, and asserts growth is below iterations / 8. It also adds value assertions (every read returns exactly the expected method) and a lowercase-normalization case, and factors the two test.each blocks through a shared measure() helper.

Security risks

None. This is a test-only change with no production code touched. No network, filesystem, credentials, or user input handling is involved; the localhost:3000 URL is a string literal passed to the Request constructor and never dialed.

Level of scrutiny

Low-to-moderate. The change is small, self-contained, and confined to one test file with no CODEOWNERS coverage. The methodology deserves a careful read because leak-guard tests are easy to get subtly wrong, but the retention-then-GC approach is sounder than what it replaces: retaining samples means a regressed getter's allocations cannot be swept before the final reading, and GC'ing before both readings removes noise from unrelated dead strings. The 8x margin between budget and regressed-behavior growth matches the REVIEW.md guidance that leak-guard bounds sit well below what the unfixed leak produces. The PR description reports the negative check (reading .url instead of .method fails all 8 cases with growth ≈ iterations), which is the "prove the test fails for the right reason" evidence the review rules ask for.

Other factors

The refactor tracks repo conventions closely: isASAN/isDebug branching for iteration count, test.each for the parameter matrix, module-scope imports only, no per-test timeouts, .toEqual for the structured distinct assertion, and comments that explain the non-obvious "why" (retention makes the count GC-independent) rather than restating code. The tests remain serial, which is correct since heapStats() is process-wide and the bodies are synchronous anyway. The bug hunt exited on dry_streak with no findings and no ruled-out candidates, and the PR timeline shows no outstanding third-party objections.

This branch has not been deployed

No deployments
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