Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 44 additions & 45 deletions test/js/web/request/request-method-getter.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading