Skip to content

perf: asyncpipe is 1.12x Node but NOT CPU-bound — needs event-loop/timer counters, not a sampler #8409

Description

@proggeramlug

Summary

asyncpipe runs at 1.12x Node with an instruction ratio of 1.71, but it is
not CPU-bound — a sampling profiler cannot see it. Aggregating sample over 14 runs of
an ~88 ms program yields 1 usable self-time sample. The process is almost always parked,
not running.

What that means

asyncpipe is the async/microtask-heaviest row in the corpus: async/await, Promise.all
fan-out per batch, and a setTimeout(…, 0) yield at every batch boundary
(sweep-artifacts-0819b/sources/asyncpipe.ts, function tick). The wall-clock gap is
therefore in event-loop and timer scheduling latency, not in executing user code.

Perry retires 1.36 G instructions against Node's 0.79 G on this row, so there is extra work
— it is simply spread across the scheduling machinery rather than concentrated anywhere a
sampler will catch.

Suggested approach

Sampling is the wrong instrument here. Instead:

  1. Count, don't sample. Instrument the timer/microtask path: how many timer registrations,
    wakeups, and microtask-queue drains happen per batch, and what does each cost? Compare that
    count against what the program logically requires (one setTimeout(0) per batch).
  2. Measure the floor. Write a micro-benchmark that does nothing but await new Promise(r => setTimeout(r, 0)) N times, and compare Perry against Node. That isolates per-yield latency
    from the benchmark's real work. If Perry's per-yield cost is materially higher, that alone
    explains the row.
  3. Then look at js_promise_run_microtasks, the timer wheel, and the setTimeout(0) path
    specifically — a zero-delay timer should not need a full timer-wheel round trip.

Prior work on this row, so it is not re-derived

  • asyncpipe regressed +55.6% in the 2026-08-18 sweep and was fixed by perf(codegen): skip redundant async frame capture tracking #8392 (codegen was
    marking the compiler-private async step closure's whole frame as escaped, so released box
    cells could never publish — published=0 -> 192859). It is now back to its pre-regression
    standing. This ticket is about the remaining 1.12x, which predates that regression.
  • fix(async_hooks): track scheduler resource lifecycles #8328 (async_hooks scheduler lifecycles) was measured against this row and is not a
    factor: +0.2%, overlapping distributions. queue_microtask_with_type only serves
    queueMicrotask()/process.nextTick(), neither of which asyncpipe uses. Do not revisit it.

Acceptance

asyncpipe beats Node (< 1.00x), or a written conclusion with counter evidence that the
remaining gap is inherent to the event-loop design. No other row regressed, all 19 byte-exact.


How to build and measure (shared setup)

Build with the static wrappers or you link a stale runtime and both A/B arms behave
identically — a vacuous result:

cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
export PERRY_RUNTIME_DIR=<your target>/release

The benchmark corpus and Node-oracle expected output are staged at
/Users/amlug/projects/perry/sweep-artifacts-0819b/ (sources/*.ts, expected/*.stdout).

export PERRY_NO_AUTO_OPTIMIZE=1 PERRY_NO_CACHE=1
perry sources/<row>.ts -o /tmp/x --no-cache
cmp <(/tmp/x) expected/<row>.stdout      # verify BEFORE timing
/usr/bin/time -l /tmp/x                  # instructions retired + peak RSS

Profiling: Perry strips its output binaries, so sample attributes everything to
???. Set PERRY_KEEP_SYMBOLS=1 PERRY_DEBUG_SYMBOLS=1 at compile time — the binary keeps
~1900 symbols and stays byte-exact — then read sample's "Sort by top of stack" section.
These programs are short; aggregate across ~14 runs for a usable sample count.

Constraints that apply to any fix here

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions