Skip to content

perf(browser): read a record from peers that may hold it, and fetch the next record after the read - #242

Merged
mickvandijke merged 4 commits into
mainfrom
mickvandijke/v2-1769-browser-first-record
Oct 29, 2026
Merged

mickvandijke merged 4 commits into
mainfrom
mickvandijke/v2-1769-browser-first-record

Conversation

@mickvandijke

@mickvandijke mickvandijke commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

A browser stream's first frame waits for its first record. On mainnet, this change starts the GET to that record's holder 1.9 s after the read begins instead of 4.3 s in Chromium (sooner in 10 of 10 rounds), and 2.2 s instead of 4.4 s in WebKit (5 of 6). On try.autonomi.com's own page, the median first frame fell from 5.8 to 3.1 s in Chromium (16 of 20) and from 4.0 to 3.1 s in WebKit (5 of 6), and seeking to 85% did not get slower.

What was slow. The read was traced on mainnet: main 7a432e7, 15 Chromium rounds, a warm client reading the first 1 MiB of try.autonomi.com's featured video 3 s after connecting. The holder's GET started at a median 4.1 s:

  • Early GETs went to peers that could not hold the record. A read's first hints are its bootstrap peers, which share 0–2 leading bits with the record. A median 6 GETs ran one at a time before the holder's. Each waited 0.46 s for a new connection lane, then got NotFound. Meanwhile a holder that discovery had already authenticated waited.
  • The next record's fetch competed. A header read also started the fetch of the record after it. That lookup held a third of the preconnect slots and queued ahead of the head's lookup on the same seeds.

What changes.

  • A read stops asking hints that cannot hold its record.
    • ReadProgress::listed records the peers each lookup reply lists. A record is stored by its close group, so a peer with CLOSE_GROUP_SIZE listed peers nearer the record cannot hold it. ReadProgress::may_hold is false for such a peer, and it leaves the read's hints.
    • Only proven identities are listed: the responder, which its session authenticated, and peers whose owner-signed address records find_node verified. A peer listed without a record counts for nothing.
    • Listing only removes hints from early reads. Discovery, its preconnects, the read after it, and that read's absence and closeness decisions are unchanged. Native reads never list peers.
  • A read that continues no other fetches the record after it only once it has finished. Reads that continue a stream are unchanged.

ADR-0004 gains a dated section with these measurements. The largest delay left is on the nodes: DTLS takes 1.39 s at the median after ICE connects, because a node's ICE-lite agent starts DTLS only once the browser nominates the connection, which the browser does late. That is V2-1770 (WithAutonomi/saorsa-transport#229).

Linear issue

Closes V2-1769

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

T1 proposed: there is no wire, format or node change. A browser sends fewer GETs. Reviewers should note:

  • First reads concentrate on the first holder found. Early GETs still choose uniformly among the hints that may hold the record, but the first such hint is now asked at once. In the Chromium traces, three holders served main's first reads (5, 4 and 1 of 10); with this change one served 9 of 10. In WebKit it served all 6, against 5 of 6. A holder that is full declines new sessions and is skipped for 15 s (ant-node ADR-0023).
  • A lying responder can at most suppress early reads. Unsigned listings do not count. A responder with signed records for invented identities near the record can remove hints, leaving the read to finish after discovery, which is unchanged.

Compatibility

  • Wire: none. The same requests, fewer of them.
  • Storage: none.
  • API: additive. data::network::ReadProgress gains listed and may_hold. A BrowserNetwork adapter that does not call listed keeps today's hints.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

On 3e9b2bc, which is on main 27461b2. The review fixes are in that commit.

  • cargo fmt --all -- --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo clippy -p ant-core --target wasm32-unknown-unknown --no-default-features --features browser-wasm[,test-utils] -- -D warnings: clean.

  • cargo test --lib --all: 878 passed. Five ReadProgress tests:

    • listed peers drop hints that cannot hold the record, now and in later offers;
    • a peer may hold the record until a whole close group is listed nearer;
    • listing ignores the local peer and repeats;
    • listing keeps only the nearest close group;
    • a listing that rules out no hint does not wake the read.
  • Browser bindings (node --test ./wasm-tests/*.test.mjs on a test-utils wasm-pack build): 254 passed. One earlier full run had a single failure in a read-budget test this PR does not touch; it then passed 5 of 5 alone, 3 of 3 with its file, and in the next full run. Three binding tests are new, and each fails without its behaviour:

    • a read stops asking hints once a lookup lists a close group nearer the record. It uses signed lookup replies from the mock (BrowserTestNode::signed_peer). Without listing, as on main, the read asked 4 far hints, in 3 of 3 runs.
    • peers a lookup reply lists without an owner proof do not rule out a holder. It fails in 2 of 2 runs on a build that counts unproven peers.
    • a read that continues no other fetches the record after it only once it has finished. On main the next record's GET was sent 0 ms after the read's.
  • scripts/adr-governance.py: passed.

  • Mainnet, traced (2026-10-28, 14:59–15:05 UTC). Test-utils builds of main and the branch, each with the same trace patch, read the first 1 MiB of the 54.9 MB video. Each sample ran in a fresh browser context, with the arms in rotating order. All 32 samples succeeded.

    engine rounds holder's GET, main → branch 1 MiB read, main → branch sooner GETs before the holder's
    Chromium 10 4.3 → 1.9 s 5.0 → 3.4 s 10/10 6 → 1
    WebKit (Playwright) 6 4.4 → 2.2 s 5.1 → 3.0 s 5/6 7 → 1
  • Mainnet, on try.autonomi.com's own page (15:05–15:26 UTC).

    • Arms: try cac5363, packaged twice. The copies differ only in the core: main's production WASM (7873c651…, byte-identical to SDK 0.1.4's) and this branch's (eea59cc4…).
    • Each sample: clicks Stream on the LEGO video 3 s after the page's client connects, times it to the first decoded frame, plays it for 10 s, then seeks to 85%, beyond the 32 MiB read-ahead window.
    • Results: all 52 samples played, with no page errors.
    engine rounds first frame, median sooner over 6 s seek to 85%, median sooner over 6 s
    Chromium 20 (two runs of 10) 5.8 → 3.1 s 16/20 9 → 3 5.1 → 4.1 s 14/20 5 → 4
    WebKit (Playwright) 6 4.0 → 3.1 s 5/6 1 → 1 2.9 → 2.8 s 3/6 0 → 0
    • In Chromium, the median paired saving was 1.6 s for the first frame and 1.0 s for the seek.
    • The two Chromium runs' first frames were sooner in 9 and 7 of 10.
    • The WebKit seek was level, at a median paired difference of 0.4 s later.
  • Earlier revision. ea1175a also gated preconnects and counted unproven listings. Through the SDK's createMediaSource(), it measured:

    • Chromium, 20 rounds: first frame 4.4 → 3.0 s (13/20) and seek 4.9 → 4.4 s (12/20);
    • WebKit, 6 rounds: first frame 4.8 → 3.8 s and seek 5.3 → 2.9 s (both 4/5).
  • Not run here: real Safari, Firefox, and the native e2e and devnet suites, which CI runs.

  • CI's Security Audit fails on RUSTSEC-2026-0387 (crypto-bigint 0.5.5, through elliptic-curve 0.13 and alloy). The advisory is from 2026-10-23, and the job has failed the same way on other branches since at least 2026-10-27. This PR changes no dependency; the fix needs an alloy upgrade of its own.

New dependency

none

ADR

n/a for T1. ADR-0004 (Proposed) gains the section "First record of a read (2026-10-28)".

Mitigation / rollback

Revert the PR. Both changes are local to the browser client's read path, and native reads are unaffected.

🤖 Generated with Claude Code

mickvandijke and others added 2 commits October 28, 2026 12:32
…he next record after the read

A browser read's first hints are its bootstrap peers, which share few or no
leading bits with the record. Its early GETs walked them one at a time, each
paying a new connection lane and a round trip, while a holder that discovery
had already authenticated waited: 1.25-1.77 s at the median on mainnet.
Lookup replies also named peers that cannot hold the record, and their
preconnects held the slots a holder needed.

- ReadProgress records the peers each lookup reply lists. A peer with a whole
  close group listed nearer the record cannot hold it: it is dropped from the
  read's hints, and the read's preconnects skip it. A holder is in the close
  group by definition, so the rule never excludes one, reachable or not.
- A read that continues no other fetches the record after it only once it has
  finished, so that record's lookup and transfer no longer compete with the
  one the read waits for, such as a video's head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where a warm browser read's first record waited on mainnet, the two
changes, their measurements against main, the cost to holder spreading, and
what is left: the node-side DTLS delay (saorsa-transport#229), large lookup
answers and round gating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mickvandijke
mickvandijke marked this pull request as ready for review October 28, 2026 14:26
mickvandijke and others added 2 commits October 28, 2026 15:57
…p preconnects

Review fixes for the first-record read:

- A lookup reply's peers count toward `ReadProgress::listed` only when they are
  proven: the responder, which its session authenticated, and peers whose
  owner-signed address records `find_node` verified. A responder without
  signed addresses could otherwise list invented identities near the record
  and rule out its holders.
- Preconnects are no longer gated by the read's rule, so a responder that
  lists signed records for invented identities can at most suppress early
  reads; discovery and its preconnects are unchanged.
- `ReadProgress` reuses the read engine's `may_hold`, takes its lock once per
  pruning, and notifies the read only when a listing removes a hint.
- Tests: signed lookup replies from the mock (`BrowserTestNode::signed_peer`),
  a regression test for unproven listings, the close group size from Rust
  (`test_close_group_size`), a test that the listing stays bounded, and a test
  that a listing that rules out nothing does not wake the read.
- CHANGELOG entry, and ADR-0004's decision updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view fixes

The traced read and try.autonomi.com's page, both against main on 3e9b2bc,
replace the earlier revision's measurements. The DTLS note now gives the cause
V2-1770 found: the node's ICE-lite agent starts DTLS only once the browser
nominates the connection.

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

@MaidSafe-Hermes MaidSafe-Hermes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at f70c39ff08d3caa1792434d7058d32b7bf5521f2.

Both review seats agree (two model families): 2/2 seats reviewed — sol-1 (gpt-6.1-sol xhigh) and glm-1 (z-ai/glm-5.3). The incremental fix now counts only the authenticated lookup responder and peers with verified owner-signed address records; find_node binds each proof to the returned peer ID. Unproven listings cannot rule out early-read hints. The holder filter no longer gates discovery preconnects, and the shared XOR-distance predicate plus send_if_modified preserve the intended cutoff and avoid waking reads when no hint changes. The added browser regressions cover signed listings and unsigned invented identities. I found no material blocker in the re-review.

CI at this head passes the build, unit, Clippy, Browser WASM, real Chromium/local-node, E2E, Merkle E2E, docs, format, and governance checks. Security Audit remains failed; at the requester’s direction, I have excluded that out-of-scope failure from this code verdict. The focused local Cargo test attempt did not complete within the execution limit, so it is not counted as passing evidence.

@mickvandijke
mickvandijke merged commit 458c717 into main Oct 29, 2026
22 of 23 checks passed
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