Skip to content

perf(websocket): mask frames without a mask array, four bytes per step - #5902

Closed
zirkelc wants to merge 1 commit into
nodejs:mainfrom
zirkelc:perf-websocket-mask
Closed

zirkelc wants to merge 1 commit into
nodejs:mainfrom
zirkelc:perf-websocket-mask

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...

Every WebSocket message the client sends: createFrame (binary, ping, close, and text frames that take the general path) and createFastTextFrame (text messages).

Rationale

Masking allocated a four-element array per frame (generateMask()) and then XORed the payload one byte at a time, reading the key back from that array with i & 3.

  • writeMask(target, offset) writes the next four bytes of the random pool straight into the frame header, where they belong anyway. It consumes the pool exactly like generateMask() did (four bytes per frame, same refill), so no array per frame.
  • maskPayload() keeps the four key bytes in local variables and XORs four payload bytes per step, then the remaining zero to three bytes. Both frame builders use it, so there is one masking loop instead of two.
  • generateMask() stays exported for benchmarks/websocket/generate-mask.mjs, now built on writeMask(), so the refill logic exists once.

Changes

  • lib/web/websocket/frame.js: the three items above.
  • test/websocket/frame.js: a round-trip test for payload lengths 0-9 (every tail length), 125, 126, 65535 and 65536, for both builders: unmasking with the frame's own key gives back the payload. A deliberately broken tail fails it.

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
ws-frame: 16 000 text frames of 16/200 bytes (both builders) + 10 frames of 70 000 bytes -32.2% -32.7% .. -25.4% median +0.1%, -1.3% .. +1.5% 1.48x

The paired runs overstate this case: -69.2% / -67.6% on the full suite, -54.2% / -54.2% focused. With two revisions in one process, shared code sees both revisions' objects, and this case also warms up slowly (its first calls are several times slower than later ones), which widens every paired sample. The standalone number is the one to expect.

full run 1 full run 2
suite TOTAL -4.45% -3.79%
suite GEOMEAN -9.93% -9.03%

Observable surface

  • None intended. Frames are byte-identical apart from the random key; the pool is consumed in the same order and amount. createFastTextFrame still masks its input buffer in place, as before. The guard unmasks every frame of the benchmark with its own key and compares with the payload, and the new test does the same for every tail length.
  • generateMask keeps its export and return shape (an array of four numbers). writeMask and maskPayload are internal.

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/5902/head:pr-5902
node perf/ab.mts 328ab843 328ab843            # noise control, ~20 s
node perf/ab.mts 328ab843 pr-5902              # full suite
PERF_ONLY=ws-frame node perf/ab.mts 328ab843 pr-5902 --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 ws-frame --iters 100; node --expose-gc perf/solo.mts pr-5902 ws-frame --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), #5903 (perf(webidl): check ByteString code units with a native scan), #5904 (perf(cookies): split each cookie pair once in getCookies)

@tsctx

tsctx commented Sep 25, 2026

Copy link
Copy Markdown
Member

Duplicate of #5809. Please search existing pull requests before submitting.

@tsctx tsctx closed this Sep 25, 2026
@zirkelc

zirkelc commented Sep 25, 2026

Copy link
Copy Markdown
Author

Duplicate of #5809. Please search existing pull requests before submitting.

Sorry, I didn't find it because the PR was closed

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.

2 participants