Skip to content

perf(ext/web): reduce promise allocations in streams#32652

Merged
bartlomieju merged 1 commit intodenoland:mainfrom
bartlomieju:perf/streams-reduce-promise-allocations
Mar 12, 2026
Merged

perf(ext/web): reduce promise allocations in streams#32652
bartlomieju merged 1 commit intodenoland:mainfrom
bartlomieju:perf/streams-reduce-promise-allocations

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

@bartlomieju bartlomieju commented Mar 11, 2026

Summary

  • Collapses uponPromise's double .then() chain into a single .then() — the second chain only caught internal assertion errors, now handled via try/catch in wrapped handlers
  • Converts setPromiseIsHandledToTrue(PromisePrototypeThen(...)) patterns to use uponPromise directly (2 call sites)
  • Merges separate uponFulfillment + uponRejection on the same promise into a single uponPromise in readableStreamDefaultControllerCallPullIfNeeded

Each uponPromise call previously created 2 promises (double .then()); now creates 1. Combined with the other changes, this reduces promise allocations across all 37 call sites in the streams implementation.

Closes #22915

Benchmark

async function benchPipeThrough(iterations, chunks) {
  const encoder = new TextEncoder();
  const data = encoder.encode("x");
  const start = performance.now();
  for (let i = 0; i < iterations; i++) {
    const stream = new ReadableStream({
      start(controller) {
        for (let j = 0; j < chunks; j++) controller.enqueue(data);
        controller.close();
      },
    });
    const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
    while (true) {
      const { done } = await reader.read();
      if (done) break;
    }
  }
  const elapsed = performance.now() - start;
  const opsPerSec = ((iterations * chunks) / elapsed * 1000).toFixed(0);
  console.log(`pipeThrough ${chunks} chunks: ${elapsed.toFixed(1)} ms (${opsPerSec} chunks/s)`);
}

async function benchPipeTo(iterations, chunks) {
  const start = performance.now();
  for (let i = 0; i < iterations; i++) {
    const readable = new ReadableStream({
      start(controller) {
        for (let j = 0; j < chunks; j++) controller.enqueue(j);
        controller.close();
      },
    });
    const writable = new WritableStream({ write() {} });
    await readable.pipeTo(writable);
  }
  const elapsed = performance.now() - start;
  const opsPerSec = ((iterations * chunks) / elapsed * 1000).toFixed(0);
  console.log(`pipeTo ${chunks} chunks: ${elapsed.toFixed(1)} ms (${opsPerSec} chunks/s)`);
}

async function benchTransformStream(iterations, chunks) {
  const start = performance.now();
  for (let i = 0; i < iterations; i++) {
    const transform = new TransformStream({
      transform(chunk, controller) { controller.enqueue(chunk); },
    });
    const writer = transform.writable.getWriter();
    const reader = transform.readable.getReader();
    const writePromise = (async () => {
      for (let j = 0; j < chunks; j++) await writer.write(j);
      await writer.close();
    })();
    while (true) {
      const { done } = await reader.read();
      if (done) break;
    }
    await writePromise;
  }
  const elapsed = performance.now() - start;
  const opsPerSec = ((iterations * chunks) / elapsed * 1000).toFixed(0);
  console.log(`TransformStream ${chunks} chunks: ${elapsed.toFixed(1)} ms (${opsPerSec} chunks/s)`);
}

// Warmup
await benchPipeThrough(3, 100);
await benchPipeTo(3, 100);
await benchTransformStream(3, 100);

console.log("\n--- Benchmark ---");
await benchPipeThrough(100, 1000);
await benchPipeTo(500, 1000);
await benchTransformStream(500, 1000);

Results (debug build, avg of 3 runs)

Benchmark Baseline (chunks/s) This PR (chunks/s) Change
pipeThrough (100×1000) 393K 407K +3.6%
pipeTo (500×1000) 2,380K 2,517K +5.7%
TransformStream (500×1000) 1,673K 1,950K +16.6%

Test plan

  • cargo test unit::streams passes
  • deno lint ext/web/06_streams.js passes
  • CI

🤖 Generated with Claude Code

Reduces the number of promise allocations in the web streams
implementation by:

1. Collapsing `uponPromise`'s double `.then()` chain into a single
   `.then()` — the second chain was only for catching internal
   assertion errors, which can be handled via try/catch in the
   wrapped handlers instead.

2. Converting `setPromiseIsHandledToTrue(PromisePrototypeThen(...))`
   patterns to use the optimized `uponPromise` directly, saving one
   promise allocation per call site.

3. Merging separate `uponFulfillment` + `uponRejection` calls on the
   same promise (in `readableStreamDefaultControllerCallPullIfNeeded`)
   into a single `uponPromise` call, halving the promise allocations.

Ref: denoland#22915

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bartlomieju bartlomieju merged commit 40b56f7 into denoland:main Mar 12, 2026
113 checks passed
@bartlomieju bartlomieju deleted the perf/streams-reduce-promise-allocations branch March 12, 2026 07:58
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.

Any perf optimization possible for uponPromise in deno_web/06_streams.js?

2 participants