Skip to content

perf(sdk): hash the Surface package with concurrent reads, same digest - #683

Merged
miyaontherelay merged 2 commits into
mainfrom
perf/surface-tree-hash-concurrent
Oct 26, 2026
Merged

miyaontherelay merged 2 commits into
mainfrom
perf/surface-tree-hash-concurrent

Conversation

@miyaontherelay

@miyaontherelay miyaontherelay commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What

The CLI hashes the installed @relayflows/surface package while it loads an authored flow (locateSurfaceAuthority → packageTreeSha256). The authored Node child hashes it again while it loads the pinned source. Both passes awaited one readFile at a time over the ~600 files of Surface 2.0.54.

packageTreeSha256 now reads up to 32 files at once. It still passes each file to the hash in the sorted walk order, so the digest is byte-identical. After a read fails, no new read starts. Once the reads already in flight settle, it throws the lowest-index failure, which is the error the serial walk would have hit first. The directory walk is unchanged (7 directories).

This is one function, so it covers both the CLI pass and the Node child pass.

Same digest

The installed published Surface 2.0.54 hashes to the digest Cloud admits for 2.0.54 (relayflow-v2-surface-authority.generated.ts: 5b9ea420db78f4c76be3c5f97815fb4d7443419ece1b56fa74d60ac05d29b47a), with both the old code and the new code:

$ git stash && bun surface-digest.ts packages/sdk/node_modules/@relayflows/surface 1   # main 2cd95614
{"runtime":"bun 1.4.2","digest":"5b9ea420db78f4c76be3c5f97815fb4d7443419ece1b56fa74d60ac05d29b47a",...}
$ git stash pop && bun surface-digest.ts packages/sdk/node_modules/@relayflows/surface 1  # this branch
{"runtime":"bun 1.4.2","digest":"5b9ea420db78f4c76be3c5f97815fb4d7443419ece1b56fa74d60ac05d29b47a",...}

I also ran 60 fresh processes alternating the old function (a verbatim copy) and the new one, on Bun 1.4.2 and Node 22.23.4. All 60 produced that one digest.

Timing

The hash alone. First call in a fresh process on the real 2.0.54 tree, 15 runs each, macOS arm64, page cache warm:

runtime serial median (min–max) ms concurrent median (min–max) ms
Bun 1.4.2 19.2 (16.1–94.2) 7.4 (6.6–9.1)
Node 22.23.4 43.5 (36.2–51.4) 19.4 (18.0–20.8)

End to end, per phase. These are standalone bun-darwin-arm64 builds of main and of this branch, each with the same measurement-only marks added (never committed; script below). Each run is flows run bench.flow.ts in a project that has the Cloud Bun envelope around Surface 2.0.54, with relayflowd already started, as Cloud does. The flow's first step writes a timestamp. There were 20 interleaved rounds; the machine was shared, with load average 25–60.

Phase (median ms) main (n=20) pr1 (n=20)
Bun CLI start (spawn to runDirectFlow) 156 156
Load flow in Bun 29 25
of which Surface hash #1 18 15
Daemon connect 3 3
Node probe and hash #1 70 69
Parallel eligibility 23 23
Admit, dispatch, authority #2 22 23
Write and re-check payload 12 13
Node child start to modules loaded 154 154
Watchdog and child hashes 80 77
Child loads pinned source 101 80
of which Surface hash #2 41 19
Journal connect to first step 21 21
Spawn to first step 673 640

The Surface hash in the Node child drops from 41 to 19 ms. The hash in Bun drops by about 3 ms, because Bun's serial reads were already fast on a warm local disk. Spawn to first step improves by 33 ms at the median here. A second batch of 20 rounds gave 703 → 667 ms.

This was measured on macOS with a warm cache, not on E2B. My earlier E2B profile of this path had the Bun load phase at about 356 ms, so the saving there should be larger, but that is an estimate and has not been measured.

Tests (fail on revert)

New file tests/authored-surface-hash.test.ts:

  • hashes the installed @relayflows/surface exactly as an in-test copy of the serial walk does
  • reads files concurrently (80 files, with reads delayed so they finish out of order) and still matches the serial digest
  • starts no new read after a failure, and throws the error the serial walk meets first

Mutation checks. For each one I reverted or changed the code, ran npx vitest run tests/authored-surface-hash.test.ts, and restored the file byte for byte with cp/cmp:

Mutation Result
src/authored-flow-loader.ts back to main (serial reads) × reads files concurrently and still hashes them in path order → expected 1 to be greater than 1 (1 failed, 2 passed)
hash each file in completion order × hashes the installed @relayflows/surface exactly as the serial walk does → expected '5312886e…' to be '5b9ea420…' and × reads files concurrently… → expected '5ea2efe3…' to be 'c89ecb55…'
throw the first failure in time, not the lowest index × starts no new read after a failure… → expected … 'unreadable dist/group-0/file-03.js' but got 'unreadable dist/group-0/file-05.js'
keep starting reads after a failure × starts no new read after a failure… → expected 81 to be less than 80
restored Tests 3 passed (3)

Verification (Node 22.23.4, Bun 1.4.2, at 999be9c)

$ npm run typecheck        # exit 0
$ npm run build            # ok
$ npm run typecheck:tests  # ok
$ npx vitest run tests/authored-parallel-*.test.ts tests/authored-flow-lifecycle-executor.test.ts tests/daemon-lifecycle.test.ts tests/daemon-lifecycle-live.test.ts tests/mcp-lifecycle.test.ts tests/authored-node-runtime.test.ts tests/authored-node-authority.test.ts tests/authored-surface-authority.test.ts tests/cloud-answer.test.ts tests/authored-root.test.ts tests/authored-flow.test.ts tests/authored-surface-hash.test.ts
 Test Files  2 failed | 14 passed (16)
      Tests  4 failed | 327 passed | 7 skipped (338)
  • authored-flow-lifecycle-executor.test.ts, 3 failures: the three completes a signal wait after … round(s) cases. These fail identically on main 2cd9561 on this machine (Tests 3 failed | 48 passed (51)). This PR does not change them.
  • authored-parallel-run-live.test.ts, 1 failure: resumes 'llm' branches without replaying completed effects failed once in that run, with run_admission_conflict, at load average 60–80. Running it alone on this branch it passed 3 of 3 times. A rerun of the whole file on this branch passed: Tests 35 passed | 7 skipped (42).
Measurement harness (local only)

apply-trace.py adds startupMark(label) calls, which append pid label epoch-ms to $FLOWS_STARTUP_TRACE. The calls go in runDirectFlow (start, around checkAuthoredTriggers, after connect), around assertAuthoredRuntimeAvailable in executeDurableAuthoredFlow, after eligibility, after the runner resolves the authority #2, before the Node child is spawned, at the start and end of packageTreeSha256, and in authored-node-entry.ts (modules loaded, ready, around loadPinnedAuthoredSource, after the journal connect). The binaries are built with node scripts/build-standalone-cli.mjs bun-darwin-arm64, and the checkout is restored afterwards.

The flow:

import { flow } from '@relayflows/surface';
export default flow('startup-bench', async f => {
  await f.run(`perl -MTime::HiRes=time -e 'printf "%.1f", time*1000' > first-step-at`);
  f.done('success');
});

Review round 1 (e5053b2)

  • Read-ahead is bounded by an in-order window of 32 files. A slow early file now stops new reads instead of letting later files buffer. The test reads ahead at most 32 files past one that is slow fails against 999be9c with expected 81 to be less than or equal to 32.
  • The failure test pins two more things. No read may start after the first failure (expected 35 to be 32 when the stop is removed), and the rejection must wait for a read still in flight (expected Infinity to be less than or equal to … when allSettled is removed).
  • Digest unchanged. Still 5b9ea420…b47a for Surface 2.0.54 on Bun and on Node 22.
  • Tests (Node 22.23.4, at e5053b2): Tests 3 failed | 329 passed | 7 skipped (339) over the same suites. The 3 failures are the completes a signal wait after … round(s) cases, which also fail on main.
  • CI linux-x64-artifact: pass at e5053b2. The run at 999be9c failed only tests/mcp.test.ts > MCP preflight and transports > waits for child close when the transport cannot own a process group with expected 999 to be greater than or equal to 1000. That is a 1 ms timer-boundary assertion in the MCP transport, which this PR does not touch.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Agent Relay sessions

  • claude session a7546818 · opened via gh pr create · last active 2026-10-26

The CLI hashes the installed @relayflows/surface tree while loading an
authored flow, and the authored Node child hashes it again while loading the
pinned source. Both awaited one readFile at a time over ~600 files.

Read with up to 32 reads in flight and feed each file to the hash strictly in
the sorted walk order, so the digest is byte-identical. After a failure no new
read starts, and the lowest-index failure is thrown, which is the error the
serial walk meets first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7bf6e0f4-78f4-44b0-a414-80c11066ee71
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both previous findings are addressed.

Summary

Reads Surface package files concurrently while preserving the existing digest.

  • Surface package hashing reads up to 32 files at once.

Reviews (2) · Last reviewed commit: "perf(sdk): bound the Surface hash read-a..." · Reviewed by Greptile

…ads in flight

Review (#683): 32 workers bounded reads in flight but not the buffers waiting
for an earlier, slow file, so the whole package could pile up in memory. Read
through an in-order window of 32 files instead: a slow file stops new reads.
After any failure no read starts, every started read settles, and the
lowest-index failure is thrown, as before.

Tests now pin the read-ahead bound, that no read starts after the first
failure, and that the rejection waits for a read still in flight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@miyaontherelay
miyaontherelay merged commit 3d40ae1 into main Oct 26, 2026
9 checks passed
@miyaontherelay
miyaontherelay deleted the perf/surface-tree-hash-concurrent branch October 26, 2026 01:47
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.

1 participant