From 29e82c2bf7aa0b38bfb33b9718bb18fd479c4b74 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 29 Aug 2026 10:25:43 +0000 Subject: [PATCH] test: make request-method-getter.test.ts faster and stricter 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. --- .../web/request/request-method-getter.test.ts | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/test/js/web/request/request-method-getter.test.ts b/test/js/web/request/request-method-getter.test.ts index bc990368bdd4..c2bd160090a7 100644 --- a/test/js/web/request/request-method-getter.test.ts +++ b/test/js/web/request/request-method-getter.test.ts @@ -1,56 +1,55 @@ import { heapStats } from "bun:jsc"; import { expect, test } from "bun:test"; +import { isASAN, isDebug } from "harness"; -const requestOptions = [ - ["http://localhost:3000/"], - [ - "http://localhost:3000/", - { - method: "GET", - }, - ], - [ - "http://localhost:3000/", - { - method: "POST", - }, - ], -] as const; -test.each(requestOptions)("new Request(%s).clone().method doesnt create a new JSString every time", function () { - // Start at a clean state. - Bun.gc(true); +// A getter that allocates one JSString per call leaves `iterations` live +// strings behind, 8x the budget. The cached string adds none (a handful of +// unrelated strings is the observed noise). +const iterations = isASAN || isDebug ? 1024 : 8192; +const budget = iterations / 8; + +// [constructor arguments after the URL, expected `method`] +const cases: [[] | [RequestInit], string][] = [ + [[], "GET"], + [[{ method: "GET" }], "GET"], + [[{ method: "POST" }], "POST"], + // The constructor normalizes a lowercase method to the uppercase form. + [[{ method: "post" }], "POST"], +]; - // @ts-expect-error - const request = new Request(...arguments); +function liveStrings(): number { + Bun.gc(true); + return heapStats().objectTypeCounts.string; +} - const { - objectTypeCounts: { string: initialStrings }, - } = heapStats(); - for (let i = 0; i < 1024 * 512; i++) { - request.clone().method; +// Keeping every returned string alive makes the count independent of GC +// timing: a per-call allocation stays live until the final reading. +function measure(read: () => string) { + const samples: string[] = []; + const before = liveStrings(); + for (let i = 0; i < iterations; i++) { + samples.push(read()); } - const { - objectTypeCounts: { string: finalStrings }, - } = heapStats(); + const growth = liveStrings() - before; + return { distinct: [...new Set(samples)], growth }; +} - expect(finalStrings - initialStrings).toBeLessThan(512); -}); +test.each(cases)( + "new Request(url, ...%j).clone().method is %s and does not allocate a JSString per call", + (init, method) => { + const request = new Request("http://localhost:3000/", ...init); + expect(request.method).toBe(method); -test.each(requestOptions)("new Request(%s).method doesnt create a new JSString every time", function () { - // Start at a clean state. - Bun.gc(true); + const { distinct, growth } = measure(() => request.clone().method); - const { - objectTypeCounts: { string: initialStrings }, - } = heapStats(); - for (let i = 0; i < 1024 * 128; i++) { - // @ts-expect-error - const request = new Request(...arguments); - request.method; - } - const { - objectTypeCounts: { string: finalStrings }, - } = heapStats(); + expect(distinct).toEqual([request.method]); + expect(growth, `${growth} new JSStrings after ${iterations} clone().method reads`).toBeLessThan(budget); + }, +); + +test.each(cases)("new Request(url, ...%j).method is %s and does not allocate a JSString per call", (init, method) => { + const { distinct, growth } = measure(() => new Request("http://localhost:3000/", ...init).method); - expect(finalStrings - initialStrings).toBeLessThan(512); + expect(distinct).toEqual([method]); + expect(growth, `${growth} new JSStrings after ${iterations} new Request().method reads`).toBeLessThan(budget); });