Skip to content

perf(webidl): check ByteString code units with a native scan - #5903

Closed
zirkelc wants to merge 1 commit into
nodejs:mainfrom
zirkelc:perf-webidl-bytestring
Closed

zirkelc wants to merge 1 commit into
nodejs:mainfrom
zirkelc:perf-webidl-bytestring

Conversation

@zirkelc

@zirkelc zirkelc commented Sep 25, 2026 •

Copy link
Copy Markdown

Context: performance campaign. This is one of 4 PRs from a systematic performance campaign on the per-request JavaScript work in lib/web (headers, Request/Response, WebIDL, cookies, WebSocket frames), run as a research loop: 12 isolated experiments, 6 kept, 6 discarded. Scope is CPU work in one process: no case opens a socket, so nothing here says anything about network, TLS or llhttp time. Every candidate change was

  • benchmarked with an A/B harness that loads two git revisions of index.js + lib/ into one process and times 12 workloads in strict alternation: A and B run back to back inside each iteration, and the delta is the median of their per-iteration ratios, so both sides sample the same machine state. Module instances carry a load-order bias of a few percent, so the harness runs both load orders in separate child processes and combines them with a geometric mean.
  • measured on 12 deterministic workloads mirroring benchmarks/ (headers iteration, request creation, parse-headers, cookies, WebSocket frames, core request instantiation, plus a whole fetch through an explicit MockAgent passed as dispatcher). The existing mitata benchmarks load one revision per process, so an A/B with them compares two standalone runs, and run-to-run drift is larger than most effects here. The workloads call the public API, not isolated snippets. The untouched benchmarks/ suite served as an external cross-check.
  • gated by a calibrated noise floor (1.5% suite total, 1.8% geometric mean, measured with identical code on both sides), and a per-case floor for changes aimed at one path; every keep required a second confirming run.
  • verified behaviour-preserving by a characterisation guard (hashes of header entries, Request/Response fields, parsed headers, cookies, WebSocket frame round trips, core request fields, and the exact TypeError messages), a differential run of both revisions in separate processes over 30 000 generated header records and cookie strings plus hand-picked edge cases, and the gates test:unit, test:fetch (incl. webidl, busboy), test:cookies, test:websocket and lint.

The numbers below are fresh runs of this branch alone against 328ab843. "Full" = whole suite, 25 paired iterations; "focused" = the same harness limited to the targeted cases, 200 paired iterations; "standalone" = one revision per process, 8 alternating process pairs, minimum of 100 runs per process, next to an identical-code control run the same way. Headline numbers are standalone, because that is what a maintainer reproduces; the paired runs decide experiments and are shown for completeness. An identical-source control measured -1.35% total. Negative = faster.

This relates to...

webidl.converters.ByteString runs for every header name and value that passes through the public header API: new Headers(init), append, set, get, has, delete, RequestInit.headers, ResponseInit.headers.

Rationale

The step "if any element of x is greater than 255, throw" ran as a JavaScript loop over every code unit. Measured in isolation, that loop was half the cost of converting a header record. A regular expression test for a code unit in \u0100-\uffff does the same check natively, about 3x faster on realistic header names and values. Only when it matches does search() find the index for the unchanged error message. Without the u flag the pattern matches single UTF-16 code units, so lone surrogates still fail, exactly like the loop.

(test() then search() rather than search() alone, because search() was about 30% slower than test() on the success path.)

Changes

  • lib/web/webidl/index.js: the check above.

Features

N/A

Bug Fixes

N/A

Breaking Changes and Deprecations

N/A

Verification (this branch vs 328ab843)

Headline numbers are standalone: one revision per process, 8 alternating process pairs, minimum of 100 runs per process, with an identical-code control (base against base) run the same way.

case standalone median (8 pairs) pair range identical-code control speed-up
headers-record: new Headers(record) + reads -10.4% -17.8% .. -2.5% median -1.7%, -7.2% .. +9.1% 1.12x
cookies: getCookies / getSetCookies / setCookie -9.2% -14.2% .. -5.0% median +1.2%, -4.2% .. +6.3% 1.10x

The paired runs this branch was verified with (they decide experiments, they do not set the claim):

case full run 1 full run 2 focused 1 focused 2
headers-record -6.63% -8.27% -7.03% -7.68%
cookies -6.20% -5.50% -7.11% -7.70%
headers-append-iterate -1.77% -7.58% (noisy case) -3.38% -5.43%
suite TOTAL -1.11% -1.97%
suite GEOMEAN -1.32% -2.38%

Observable surface

  • None intended. The error message is built from the same first code unit above 255, so the TypeError text is identical (the guard hashes it, and a differential run over 30 000 generated records with characters above 255 and surrogate pairs is identical).

Invariants this relies on

  • The regular expression has no g or y flag, so test() and search() keep no state between calls.

Open questions for maintainers (not in this PR)

  • A header named __proto__ in a record is silently dropped: new Headers(JSON.parse('{"__proto__":"x"}')) has no entries, because the record converter assigns keys on a plain {}. Browsers keep the header. Fixing it changes behaviour, so it is not part of this PR.

WPT (npm run test:wpt: /fetch, /mimesniff, /xhr, /websockets, /eventsource) was run locally on 328ab843 and on this branch: per test and per case, pass/fail is identical across all 1 282 test files.

Note on CI: Test with Node.js 22 compiled --without-intl already fails on main at 328ab843, before this change.

Reproducing the numbers

The harness (A/B runner, guard, differential, focused and standalone runners) lives in perf/ on the campaign branch: zirkelc/undici@afde9423/perf (the plan and the experiment log with all discarded experiments are in the same directory). It needs Node 24 (it runs .mts directly) and no install beyond npm ci.

git fetch https://github.com/zirkelc/undici.git afde94231e5e3ac6ee5a874e20f2b8b1eae800df
git checkout afde94231e5e3ac6ee5a874e20f2b8b1eae800df -- perf && git reset -q -- perf   # harness as untracked files
git fetch origin pull/5903/head:pr-5903
node perf/ab.mts 328ab843 328ab843            # noise control, ~20 s
node perf/ab.mts 328ab843 pr-5903              # full suite
PERF_ONLY=headers-record,cookies,headers-append-iterate node perf/ab.mts 328ab843 pr-5903 --iters 200   # focused
# standalone: one revision per process, alternating, 8 pairs
for i in 1 2 3 4 5 6 7 8; do node --expose-gc perf/solo.mts 328ab843 headers-record --iters 100; node --expose-gc perf/solo.mts pr-5903 headers-record --iters 100; done

A row marked ? has an interquartile band that crosses zero in that run; judge by two runs that agree, and by the standalone numbers.

perf/cases.mts, the workloads these numbers come from
/**
 * Cases for the undici campaign.
 *
 * Scope: the per-request JavaScript work that every `fetch` pays on top of the network, in one
 * process. No case opens a socket: the network is replaced by an explicit `MockAgent` passed as
 * `dispatcher`, never through the global dispatcher (that lives on `globalThis` and would be shared
 * by both revisions in the process).
 *
 * Workloads mirror `benchmarks/` (headers iteration, request creation, parse-headers, cookies,
 * websocket frames) but call the public API, so inlining and hidden-class effects of the composed
 * path are part of the measurement. All data is seeded or literal.
 */
import { type PerfCase, rng } from "./harness.mts";

const ORIGIN = "https://example.com";

/** Realistic response headers, the same sets as benchmarks/core/parse-headers.mjs. */
const HEADER_SETS: Array<Record<string, string>> = [
  {
    "Content-Type": "application/json",
    Date: "Wed, 01 Nov 2023 00:00:00 GMT",
    "Powered-By": "NodeJS",
    "Content-Encoding": "gzip",
    "Set-Cookie": "__Secure-ID=123; Secure; Domain=example.com",
    "Content-Length": "150",
    Vary: "Accept-Encoding, Accept, X-Requested-With",
  },
  { "Content-Type": "text/html; charset=UTF-8", "Content-Length": "1234", Date: "Wed, 06 Dec 2023 12:47:57 GMT", Server: "Bing" },
  {
    "Content-Type": "image/jpeg",
    "Content-Length": "56789",
    Date: "Wed, 06 Dec 2023 12:48:12 GMT",
    Server: "Bing",
    ETag: '"a1b2c3d4e5f6g7h8i9j0"',
  },
  {
    Cookie: "session_id=1234567890abcdef",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    Host: "www.bing.com",
    Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br",
  },
  {
    Location: "https://www.bing.com/search?q=bing",
    Status: "302 Found",
    Date: "Wed, 06 Dec 2023 12:48:27 GMT",
    Server: "Bing",
    "Content-Type": "text/html; charset=UTF-8",
    "Content-Length": "0",
  },
  {
    "Content-Type": "application/json; charset=UTF-8",
    "Content-Length": "2345",
    Date: "Wed, 06 Dec 2023 12:48:42 GMT",
    Server: "Bing",
    Status: "200 OK",
    "Cache-Control": "no-cache, no-store, must-revalidate",
  },
];

const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
function word(rand: () => number, length: number): string {
  let out = "";
  for (let i = 0; i < length; i++) out += CHARS[Math.floor(rand() * CHARS.length)];
  return out;
}

/** Captures the observable parts of a thrown value, including the exact message (WPT asserts it). */
function attempt(fn: () => unknown): unknown {
  try {
    const value = fn();
    return { ok: true, value: value === undefined ? null : String(value) };
  } catch (error: any) {
    return { ok: false, name: error?.name, message: error?.message, code: error?.code ?? null };
  }
}

function headerEntries(headers: any): Array<[string, string]> {
  return [...headers];
}

function requestFields(r: any): unknown {
  return {
    method: r.method,
    url: r.url,
    headers: headerEntries(r.headers),
    destination: r.destination,
    referrer: r.referrer,
    referrerPolicy: r.referrerPolicy,
    mode: r.mode,
    credentials: r.credentials,
    cache: r.cache,
    redirect: r.redirect,
    integrity: r.integrity,
    keepalive: r.keepalive,
    isReloadNavigation: r.isReloadNavigation,
    isHistoryNavigation: r.isHistoryNavigation,
    duplex: r.duplex,
    bodyUsed: r.bodyUsed,
    hasBody: r.body !== null,
    aborted: r.signal.aborted,
  };
}

function responseFields(r: any): unknown {
  return {
    type: r.type,
    url: r.url,
    redirected: r.redirected,
    status: r.status,
    ok: r.ok,
    statusText: r.statusText,
    headers: headerEntries(r.headers),
    bodyUsed: r.bodyUsed,
    hasBody: r.body !== null,
  };
}

/**
 * `PERF_ONLY=name[,name]` limits every harness script to those cases, so a per-case keep can be
 * confirmed with many more paired iterations in the time one full run takes.
 */
export function buildCases(lib: any): Array<PerfCase> {
  const only = process.env.PERF_ONLY?.split(",");
  const cases = allCases(lib);
  return only ? cases.filter((c) => only.includes(c.name)) : cases;
}

function allCases(lib: any): Array<PerfCase> {
  const { Headers, Request, Response, MockAgent, fetch, getCookies, getSetCookies, setCookie, coreUtil, wsFrame, coreRequest } = lib;
  const cases: Array<PerfCase> = [];

  /** Headers from a record, then the reads a request pipeline does. */
  {
    const build = (init: Record<string, string>) => {
      const h = new Headers(init);
      h.get("content-type");
      h.get("content-length");
      h.has("set-cookie");
      return h;
    };
    cases.push({
      name: "headers-record",
      run: () => {
        for (let i = 0; i < 3_000; i++) build(HEADER_SETS[i % HEADER_SETS.length]);
      },
      collect: () => HEADER_SETS.map((s) => headerEntries(build(s))),
      alloc: () => new Headers(HEADER_SETS[0]),
    });
  }

  /** append() with mixed-case names, then sorted iteration (mirrors benchmarks/fetch/headers.mjs). */
  {
    const rand = rng(7);
    const sets = [4, 8, 16, 32, 64].map((n) =>
      Array.from({ length: n }, (_, i) => [i % 3 === 0 ? word(rand, 12).toUpperCase() : word(rand, 12), word(rand, 20)] as [string, string])
    );
    const build = (pairs: Array<[string, string]>) => {
      const h = new Headers();
      for (const [k, v] of pairs) h.append(k, v);
      h.append("Set-Cookie", "a=1");
      h.append("set-cookie", "b=2");
      return h;
    };
    cases.push({
      name: "headers-append-iterate",
      run: () => {
        for (let i = 0; i < 100; i++) {
          for (const pairs of sets) {
            const h = build(pairs);
            for (const _ of h);
            h.getSetCookie();
          }
        }
      },
      collect: () => sets.map((p) => {
        const h = build(p);
        return [headerEntries(h), h.getSetCookie()];
      }),
    });
  }

  /** Failure paths of the header model: webidl and validation errors with exact messages. */
  {
    const bad: Array<() => unknown> = [
      () => new Headers([["a"]] as any),
      () => new Headers([["a", "b", "c"]] as any),
      () => new Headers({ "bad name": "x" }),
      () => new Headers({ ok: "bad\nvalue" }),
      () => new Headers(42 as any),
      () => new Headers().append("", "x"),
      () => new Headers().get("in valid"),
    ];
    cases.push({
      name: "headers-invalid",
      run: () => {
        for (let i = 0; i < 1_400; i++) attempt(bad[i % bad.length]);
      },
      collect: () => bad.map(attempt),
    });
  }

  /** new Request(url): the minimal constructor path (mirrors benchmarks/fetch/request-creation.mjs). */
  {
    const urls = Array.from({ length: 16 }, (_, i) => `${ORIGIN}/api/v1/items/${i}?page=${i}&q=search`);
    cases.push({
      name: "request-url",
      run: () => {
        for (let i = 0; i < 4_000; i++) new Request(urls[i & 15]);
      },
      collect: () => urls.slice(0, 4).map((u) => requestFields(new Request(u))),
      alloc: () => new Request(urls[0]),
    });
  }

  /** new Request(url, init) with method, headers and a JSON body: the typical POST. */
  {
    const body = JSON.stringify({ id: 1, name: "item", tags: ["a", "b", "c"] });
    const init = (i: number) => ({
      method: "POST",
      headers: { "content-type": "application/json", authorization: `Bearer token-${i & 7}`, "x-request-id": `req-${i & 7}` },
      body,
    });
    cases.push({
      name: "request-init",
      run: () => {
        for (let i = 0; i < 1_200; i++) new Request(`${ORIGIN}/api/items`, init(i));
      },
      collect: () => [0, 1].map((i) => requestFields(new Request(`${ORIGIN}/api/items`, init(i)))),
      alloc: () => new Request(`${ORIGIN}/api/items`, init(0)),
    });
  }

  /**
   * Request clone and Request-from-Request, as middleware and retries do. Each derived request adds
   * an abort listener to its source's signal, so the base is rebuilt every 10 derivations: the
   * listener count per signal stays realistic and the cost of one run does not grow with the number
   * of runs before it. Many requests on one shared signal is a scaling shape (see buildScan).
   */
  {
    const init = { headers: HEADER_SETS[3] };
    const url = `${ORIGIN}/api/items?x=1`;
    cases.push({
      name: "request-clone",
      run: () => {
        for (let i = 0; i < 90; i++) {
          const base = new Request(url, init);
          for (let k = 0; k < 10; k++) {
            base.clone();
            new Request(base, { method: "PUT" });
          }
        }
      },
      collect: () => {
        const base = new Request(url, init);
        return [requestFields(base.clone()), requestFields(new Request(base, { method: "PUT" }))];
      },
    });
  }

  /** Response construction: new Response(body, init) and Response.json(). */
  {
    const payload = { id: 1, items: [1, 2, 3], ok: true };
    cases.push({
      name: "response-new",
      run: () => {
        for (let i = 0; i < 800; i++) {
          new Response("hello world", { status: 201, statusText: "Created", headers: HEADER_SETS[i % HEADER_SETS.length] });
          Response.json(payload);
        }
      },
      collect: () => [
        responseFields(new Response("hello world", { status: 201, statusText: "Created", headers: HEADER_SETS[0] })),
        responseFields(Response.json(payload)),
        responseFields(Response.error()),
        responseFields(Response.redirect(`${ORIGIN}/x`, 302)),
        attempt(() => new Response(null, { status: 99 })),
        attempt(() => new Response("x", { status: 204 })),
        attempt(() => new Response(null, { statusText: "bad\n" })),
      ],
      alloc: () => new Response("hello world", { status: 200, headers: HEADER_SETS[1] }),
    });
  }

  /** Raw header parsing from the wire (mirrors benchmarks/core/parse-headers.mjs, both casings). */
  {
    const regular = HEADER_SETS.map((x) => Object.entries(x).flat().map((c) => Buffer.from(c)));
    const upper = HEADER_SETS.map((x) => Object.entries(x).flat().map((c) => Buffer.from(c.toUpperCase())));
    const all = [...regular, ...upper];
    cases.push({
      name: "parse-headers",
      run: () => {
        for (let r = 0; r < 500; r++) {
          for (let i = 0; i < all.length; i++) {
            coreUtil.parseHeaders(all[i]);
            coreUtil.parseRawHeaders(all[i]);
          }
        }
      },
      collect: () => all.map((h) => [coreUtil.parseHeaders(h), coreUtil.parseRawHeaders(h)]),
    });
  }

  /** Cookie parsing and serialisation on Headers. */
  {
    const rand = rng(11);
    const cookieHeader = Array.from({ length: 12 }, () => `${word(rand, 8)}=${word(rand, 24)}`).join("; ");
    const setCookies = [
      "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly",
      "__Host-session=xyz; Path=/; Secure; SameSite=Strict",
      "lang=en-US; Max-Age=3600; Domain=example.com; Path=/docs",
      "theme=dark; SameSite=Lax",
      "bad",
    ];
    const cookie = { name: "token", value: "abc123", path: "/", secure: true, httpOnly: true, sameSite: "Lax", maxAge: 3600, domain: "example.com" };
    const reqHeaders = () => new Headers({ cookie: cookieHeader });
    const resHeaders = () => {
      const h = new Headers();
      for (const s of setCookies) h.append("set-cookie", s);
      return h;
    };
    cases.push({
      name: "cookies",
      run: () => {
        for (let i = 0; i < 500; i++) {
          getCookies(reqHeaders());
          getSetCookies(resHeaders());
          setCookie(new Headers(), cookie);
        }
      },
      collect: () => {
        const h = new Headers();
        setCookie(h, cookie);
        return [getCookies(reqHeaders()), getSetCookies(resHeaders()), headerEntries(h)];
      },
    });
  }

  /** WebSocket frame encoding for text payloads of three length classes. */
  {
    const rand = rng(5);
    const payloads = [16, 200, 70_000].map((n) => Buffer.from(word(rand, n)));
    /**
     * The mask is random, so the sample keeps the deterministic parts: the header bytes without the
     * mask, and whether unmasking the body with the frame's own mask gives back the payload.
     */
    const unmask = (head: Buffer, body: Buffer, offset: number) => {
      const key = head.subarray(offset - 4, offset);
      return Buffer.from(body.map((b, i) => b ^ key[i & 3]));
    };
    const offsetOf = (head: Buffer) => ((head[1] & 0x7f) === 127 ? 14 : (head[1] & 0x7f) === 126 ? 8 : 6);
    const sampleFrame = (p: Buffer) => {
      const frame: Buffer = new wsFrame.WebsocketFrameSend(p).createFrame(0x1);
      const offset = offsetOf(frame);
      return [frame.length, [...frame.subarray(0, offset - 4)], unmask(frame, frame.subarray(offset), offset).equals(p)];
    };
    const sampleFast = (p: Buffer) => {
      const [head, body] = wsFrame.WebsocketFrameSend.createFastTextFrame(Buffer.from(p));
      const offset = offsetOf(head);
      return [head.length, body.length, [...head.subarray(0, offset - 4)], unmask(head, body, offset).equals(p)];
    };
    cases.push({
      name: "ws-frame",
      run: () => {
        for (let i = 0; i < 8_000; i++) {
          const p = payloads[i % 2];
          new wsFrame.WebsocketFrameSend(p).createFrame(0x1);
          /** Masks its input in place; the XOR work is the same whatever the bytes are. */
          wsFrame.WebsocketFrameSend.createFastTextFrame(p);
        }
        for (let i = 0; i < 10; i++) new wsFrame.WebsocketFrameSend(payloads[2]).createFrame(0x2);
      },
      collect: () => payloads.map((p) => [sampleFrame(p), sampleFast(p)]),
    });
  }

  /**
   * Dispatcher-level request construction (lib/core/request.js), built once per dispatch by Client,
   * Pool and Agent: header record, header array and a string body (mirrors
   * benchmarks/core/request-instantiation.mjs, with realistic headers). Added in experiment 8; its
   * guard expectation was recorded against the campaign base revision.
   */
  {
    const CoreRequest = coreRequest.default ?? coreRequest;
    const handler = new lib.DecoratorHandler({});
    const origin = "https://example.com";
    const record = { ...HEADER_SETS[3], "content-type": "application/json", "x-request-id": "abc" };
    const array = Object.entries(HEADER_SETS[5]).flat();
    const body = JSON.stringify({ id: 1, name: "item" });
    const make = (i: number) =>
      i % 3 === 0
        ? new CoreRequest(origin, { path: "/api/items?page=1", method: "GET", headers: record, body: null }, handler)
        : i % 3 === 1
          ? new CoreRequest(origin, { path: "/api/items", method: "POST", headers: array, body }, handler)
          : new CoreRequest(origin, { path: "/api/items", method: "PUT", headers: record, query: { a: 1, b: "x y" }, body }, handler);
    const fields = (r: any) => [r.method, r.path, r.origin, r.protocol, r.host, r.contentLength, r.contentType, r.headers, r.idempotent, r.blocking, r.servername, r.upgrade, r.reset, r.body === null ? null : String(r.body)];
    const bad: Array<() => unknown> = [
      () => new CoreRequest(origin, { path: "api", method: "GET" }, handler),
      () => new CoreRequest(origin, { path: "/a b", method: "GET" }, handler),
      () => new CoreRequest(origin, { path: "/", method: "G E T" }, handler),
      () => new CoreRequest(origin, { path: "/", method: "GET", headers: { "bad key": "x" } }, handler),
      () => new CoreRequest(origin, { path: "/", method: "GET", headers: { a: "x\n" } }, handler),
      () => new CoreRequest(origin, { path: "/", method: "GET", headers: ["a"] }, handler),
      () => new CoreRequest(origin, { path: "/", method: "GET", headers: { "content-length": "x" } }, handler),
      () => new CoreRequest(origin, { path: "/", method: "GET", headers: { "transfer-encoding": "chunked" } }, handler),
    ];
    cases.push({
      name: "core-request",
      run: () => {
        for (let i = 0; i < 3_000; i++) make(i);
      },
      collect: () => [[0, 1, 2].map((i) => fields(make(i))), bad.map(attempt)],
      alloc: () => make(0),
    });
  }

  /**
   * Whole fetch through an explicit MockAgent: Request construction, dispatch, mock reply, Response
   * and body consumption. Async, so each call runs a fixed batch of 200 requests; the promise and
   * microtask overhead is then the same constant on both sides.
   */
  {
    let agent: any;
    const json = JSON.stringify({ id: 1, name: "item", tags: ["a", "b", "c"] });
    const one = async (i: number) => {
      const res = await fetch(`${ORIGIN}/api/items/${i & 3}`, {
        dispatcher: agent,
        headers: { accept: "application/json", "x-request-id": `r${i & 3}` },
      });
      return [res, await res.text()] as const;
    };
    const post = async () => {
      const res = await fetch(`${ORIGIN}/api/items`, { dispatcher: agent, method: "POST", body: json, headers: { "content-type": "application/json" } });
      return [res, await res.json()] as const;
    };
    cases.push({
      name: "fetch-mock",
      setup: () => {
        agent = new MockAgent();
        agent.disableNetConnect();
        const pool = agent.get(ORIGIN);
        for (let k = 0; k < 4; k++) {
          pool.intercept({ path: `/api/items/${k}`, method: "GET" }).reply(200, json, { headers: { "content-type": "application/json", etag: `"v${k}"` } }).persist();
        }
        pool.intercept({ path: "/api/items", method: "POST" }).reply(201, json, { headers: { "content-type": "application/json" } }).persist();
      },
      teardown: () => {
        agent?.close();
        agent = undefined;
      },
      run: async () => {
        const batch: Array<Promise<unknown>> = [];
        for (let i = 0; i < 160; i++) batch.push(one(i));
        for (let i = 0; i < 40; i++) batch.push(post());
        await Promise.all(batch);
      },
      collect: async () => {
        const [r1, t1] = await one(1);
        const [r2, t2] = await post();
        return [responseFields(r1), t1, responseFields(r2), t2];
      },
    });
  }

  return cases;
}

/** Scaling shapes: each takes a size n and must cost about 4x at 4n if the path is linear. */
export function buildScan(lib: any): Array<{ name: string; input: (n: number) => unknown; run: (input: any) => void; n?: number }> {
  const { Headers, Request, getCookies, coreUtil } = lib;
  const rand = rng(3);
  return [
    {
      name: "requests-shared-signal",
      n: 250,
      input: (n) => n,
      run: (n: number) => {
        const ac = new AbortController();
        for (let i = 0; i < n; i++) new Request(`${ORIGIN}/x`, { signal: ac.signal });
      },
    },
    {
      name: "request-from-request",
      n: 250,
      input: (n) => n,
      run: (n: number) => {
        const base = new Request(`${ORIGIN}/x`);
        for (let i = 0; i < n; i++) new Request(base);
      },
    },
    {
      name: "headers-many-names",
      n: 250,
      input: (n) => Array.from({ length: n }, (_, i) => [`x-${word(rand, 6)}-${i}`, "v"]),
      run: (pairs: Array<[string, string]>) => {
        const h = new Headers();
        for (const [k, v] of pairs) h.append(k, v);
        for (const _ of h);
      },
    },
    {
      name: "headers-same-name",
      n: 250,
      input: (n) => n,
      run: (n: number) => {
        const h = new Headers();
        for (let i = 0; i < n; i++) h.append("accept", `type/${i}`);
        h.get("accept");
      },
    },
    {
      name: "set-cookie-many",
      n: 250,
      input: (n) => n,
      run: (n: number) => {
        const h = new Headers();
        for (let i = 0; i < n; i++) h.append("set-cookie", `c${i}=v${i}; Path=/`);
        for (const _ of h);
        h.getSetCookie();
      },
    },
    {
      name: "cookie-header-long",
      n: 250,
      input: (n) => new Headers({ cookie: Array.from({ length: n }, (_, i) => `k${i}=${word(rand, 16)}`).join("; ") }),
      run: (h: any) => {
        getCookies(h);
      },
    },
    {
      name: "raw-headers-many",
      n: 250,
      input: (n) => Array.from({ length: n }, (_, i) => [Buffer.from(`X-Header-${i}`), Buffer.from(word(rand, 20))]).flat(),
      run: (raw: Array<Buffer>) => {
        coreUtil.parseHeaders(raw);
        coreUtil.parseRawHeaders(raw);
      },
    },
  ];
}

/**
 * Differential inputs, for changes whose risky inputs the guard does not reach. Each input is a
 * string: either a named scenario from SCENARIOS, or a JSON record of header name to value.
 */
export function buildDifferential(lib: any) {
  const { Headers, Request, Response, getCookies, getSetCookies, setCookie } = lib;
  const PIECES = ["a", "Content-Type", "x-y", " ", "\t", "\n", "\r", "\0", "ā", "é", "=", ";", ",", '"', "set-cookie", "cookie", "__proto__", "", "é", "💩", "0", "Ā"];
  const SCENARIOS: Record<string, () => unknown> = {
    "symbol-key": () => new Headers({ [Symbol("s")]: "x" } as any),
    "symbol-value": () => new Headers({ a: Symbol("v") } as any),
    "symbol-key-after-string": () => new Headers({ a: "1", [Symbol("s")]: "x" } as any),
    "wide-key": () => new Headers({ "ā": "x" }),
    "wide-value": () => new Headers({ a: "ā" }),
    "number-key": () => new Headers({ 1: "a", b: "c", 0: "d" } as any),
    "getter-order": () => {
      const log: Array<string> = [];
      const o = {};
      for (const k of ["b", "a", "ā", "c"]) Object.defineProperty(o, k, { enumerable: true, get: () => (log.push(k), "v") });
      try {
        new Headers(o);
      } catch (e: any) {
        log.push(`${e.name}: ${e.message}`);
      }
      return log;
    },
    "non-enumerable": () => {
      const o = { a: "1" };
      Object.defineProperty(o, "hidden", { enumerable: false, value: "2" });
      return new Headers(o);
    },
    "proxy-record": () => new Headers(new Proxy({ a: "1", b: "2" }, {})),
    "object-value": () => new Headers({ a: { toString: () => "x" } } as any),
    "request-record": () => new Request(`${ORIGIN}/`, { headers: { a: "1", B: "2" } }),
    "response-record": () => new Response(null, { headers: { a: "1", B: "2" } }),
    "request-symbol-header": () => new Request(`${ORIGIN}/`, { headers: { a: Symbol("q") } as any }),
    "response-symbol-header": () => new Response(null, { headers: { [Symbol("k")]: "v" } as any }),
    "derived-request-aliasing": () => {
      const base = new Request(`${ORIGIN}/`, { headers: [["Set-Cookie", "a=1"], ["x-b", "2"], ["set-cookie", "c=3"], ["X-A", "1"]] });
      [...base.headers];
      const derived = new Request(base, { method: "PUT" });
      const before = [[...derived.headers], derived.headers.getSetCookie()];
      derived.headers.append("set-cookie", "d=4");
      derived.headers.append("x-0", "0");
      base.headers.append("x-z", "z");
      return [before, [...derived.headers], derived.headers.getSetCookie(), [...base.headers], base.headers.getSetCookie(), derived.method];
    },
    "derived-request-no-cors": () => {
      const base = new Request(`${ORIGIN}/`, { mode: "no-cors", headers: { a: "1" } });
      const derived = new Request(base, { cache: "no-store" });
      return [[...derived.headers], derived.mode, derived.cache];
    },
    "derived-request-new-headers": () => {
      const base = new Request(`${ORIGIN}/`, { headers: { a: "1", "set-cookie": "x=1" } });
      const derived = new Request(base, { headers: base.headers });
      const replaced = new Request(base, { headers: { b: "2" } });
      return [[...derived.headers], derived.headers.getSetCookie(), [...replaced.headers]];
    },
  };
  const describeValue = (value: any): unknown => {
    if (value instanceof Headers) return [...value];
    if (value instanceof Request) return [value.method, value.url, [...value.headers]];
    if (value instanceof Response) return [value.status, [...value.headers]];
    return value;
  };
  return {
    fixed: [
      ...Object.keys(SCENARIOS),
      "cookie:a=b; c=d",
      "cookie:a",
      "cookie:=x",
      "cookie:a==b=c; ;x",
      "cookie: a = b ;c=",
      "cookie:__proto__=1; constructor=2",
    ],
    random: (rand: () => number) => {
      const pick = () => {
        let s = "";
        const n = Math.floor(rand() * 4);
        for (let i = 0; i < n; i++) s += PIECES[Math.floor(rand() * PIECES.length)];
        return s;
      };
      if (rand() < 0.3) return `cookie:${Array.from({ length: 1 + Math.floor(rand() * 4) }, () => `${pick()}=${pick()}`).join(rand() < 0.5 ? "; " : ";")}`;
      const rec: Record<string, string> = {};
      const n = Math.floor(rand() * 5);
      for (let i = 0; i < n; i++) rec[pick() || "k"] = pick();
      return JSON.stringify(rec);
    },
    describe: (input: string) => {
      const run = () => {
        if (input in SCENARIOS) return describeValue(SCENARIOS[input]());
        if (input.startsWith("cookie:")) {
          const h = new Headers();
          h.append("cookie", input.slice(7).replace(/[\0\r\n]/g, ""));
          const out = new Headers();
          const parsed = getCookies(h);
          for (const [name, value] of Object.entries(parsed)) {
            try {
              setCookie(out, { name, value: value as string });
            } catch (e: any) {
              return [Object.entries(parsed), `${e.name}: ${e.message}`];
            }
          }
          return [Object.entries(parsed), Object.getPrototypeOf(parsed) === null, getSetCookies(out)];
        }
        return describeValue(new Headers(JSON.parse(input)));
      };
      try {
        return { ok: run() };
      } catch (e: any) {
        return { error: `${e.name}: ${e.message}` };
      }
    },
  };
}

Status


Companion PRs from the same campaign: #5901 (perf(fetch): avoid redundant request state in the Request constructor), #5902 (perf(websocket): mask frames without a mask array, four bytes per step), #5904 (perf(cookies): split each cookie pair once in getCookies)

@KhafraDev

Copy link
Copy Markdown
Member

Before doing a "campaign" of slop, please do the bare minimum of asking the maintainers. None of us want to spend time reviewing these.

@KhafraDev KhafraDev closed this Sep 25, 2026
@zirkelc

zirkelc commented Sep 25, 2026

Copy link
Copy Markdown
Author

Sorry, I just wanted to help out by trying to find small things that can be improved.

These are single file/line changes that measurably improve the performance, even though just minimally.

But what's the "slop" in there?

@jasnell

jasnell commented Sep 25, 2026

Copy link
Copy Markdown
Member

@zirkelc ... your motivation is fine, to be honest, and using an AI agent to help is generally perfectly acceptable. The change here also is well motivated and should be considered. The regex approach is definitely way faster than the naive linear scan. That said, I think we could do better and, long term, there's no reason why we can't rely on the runtime to help us make this even faster. nodejs/node#66298

@mcollina ... I think it's likely worth re-opening this.

@KhafraDev

KhafraDev commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

I would honestly prefer to disable outside contributors entirely... people can still open issues. We all have access to ai here and therefore all of us can open, at worst, an equally acceptable PR.

There is a difference between having ai autonomously spam open PRs and actual contributors using ai to contribute.

I am not debating the validity of this PR (out of 4), I am against the methodology of spamming PRs (where at LEAST half of them are invalid) in the hopes some land.

@jasnell

jasnell commented Sep 25, 2026

Copy link
Copy Markdown
Member

For this particular change, the regex approach is faster in every case. For smaller values only marginally but it scales way better as strings get larger and more complex. I'd say this change is worth taking but we might also wait until nodejs/node#66298 lands and is available (and backported) to releases. Obviously we won't be able to rely on it entirely but a builtin + regex approach will be faster than this linear scan.

@KhafraDev

KhafraDev commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Yes, I'm not debating the validity of this one change (out of four rapid-fire low effort PRs), but the methodology by the author. Throwing things at the wall and hoping something sticks isn't an acceptable way to contribute to opensource, nor do I find it polite to ask maintainers to do the due diligence you yourself have not done. For every invalid PR, it also takes far more time to refute and review than it does for them to ask [ai agent] to open a PR (Brandolini's law, but applied to opensource).

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.

3 participants