Skip to content

perf(js): drop sorted-btree and the Manual clock from every bundle - #8671

Merged
kixelated merged 3 commits into
mainfrom
quest/m0/nightly/bundle-size
Nov 14, 2026
Merged

kixelated merged 3 commits into
mainfrom
quest/m0/nightly/bundle-size

Conversation

@kixelated

@kixelated kixelated commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Completes quest/m0/nightly/bundle-size.md.

Problem

Nightly's 30-day size check (10% limit) fails from 11-21: against the 10-22 report, js/net Brotli is 1.59x (98,909 to 157,282 bytes) and every other bundle is 1.40 to 1.48x. The maintainer chose to audit and shrink, then accept the rest as feature growth, without loosening the check.

Approach

I attributed every first-load bundle against the 10-22 Nightly metafiles (nightly-size-history of run 39779729461), counting only the static import closure, as sh/size/bundles.ts does. Two dependencies don't earn their bytes:

  • sorted-btree (19.4 KB raw, about 5 KB Brotli, in every bundle). It backs the ledger's two ordered indexes, which need only floor/ceil/next/prev lookups, insert, delete, and in-order iteration. The ledger now uses an in-tree Sorted list (js/net/src/util/sorted.ts, about 150 lines). It stores values in chunks of 128 to 512, the way Python's sortedcontainers does, so an insert or delete moves at most one chunk plus the chunk index. A peer that fragments arrivals still costs logarithmic lookups, the property moq-dev/moq#6869 added the tree for. This also removes the CommonJS interop shim.
  • @moq/clock's Manual test clock (2.3 KB raw in net and hang). It moves from @moq/clock to the existing @moq/clock/test entrypoint. @moq/net's Clock namespace re-export no longer carries it.

Everything else is feature code (below).

Bundle (Brotli) 10-22 main this PR change vs 10-22
js/net 98,909 156,956 151,568 -5,388 (-3.4%) 1.53x
js/hang 119,937 178,006 172,522 -5,484 (-3.1%) 1.44x
js/watch 133,560 196,056 191,129 -4,927 (-2.5%) 1.43x
js/watch-ui 144,409 206,890 201,883 -5,007 (-2.4%) 1.40x
js/publish 131,060 189,445 184,571 -4,874 (-2.6%) 1.41x

The rest is feature growth

About 190 KB of the roughly 200 KB of raw first-load growth is js/net, spread over about 320 commits since 10-22 (74 of them feat). The largest per-file changes:

File +raw bytes Feature
track.js +27,720 byte-pool cache (#8177), drops, floors, Finished handles
lite/subscriber.js +25,797 lite-07 SUBSCRIBE_SKIP and stale drops (#7670), YIELDED continuations (#8545), held re-offers (#8546)
ietf/subscriber.js (lazy chunk) +11,353 IETF datagram tracks (#7959)
lite/publisher.js +10,349 priority-based stream reset (#8103), YIELDED
resume.js +9,442 failover onto a re-offered copy
window.js, socket.js, cache.js, fill.js, listing.js, datagram.js +36,700, all new WebSocket receive windows, socket endpoints (#7884), the byte-pool cache, fill, path listings (#7678), split datagram tracks (#7846)
lite-07 route layer (origin.js, lite/announce.js, lite/floors.js, ...) ~+10,000 routes, warm routes, and route-key proofs (#8443, #8457, #8615)

Outside js/net, the watch player gained 12 KB raw, the publish encoder 9 KB, and @moq/ui 5.5 KB, all features. Zod shrank, and async-mutex, uuid, and @svta/cml-iso-bmff left the first load.

What this means for Nightly

The 30-day check still fails until a post-growth report becomes its anchor. Nightly recorded no report from 11-08 to 11-12, so the anchor stays at 11-07 from 12-08 to 12-12. Against 11-07, this PR brings hang, watch-ui, and publish within 10%, but js/net is 1.107x (151,568 vs 136,880; the limit is 150,568). From 12-13 the 11-13 report becomes the anchor. This is the outcome the quest's Plan accepted.

Choices made without the maintainer

  • I replaced sorted-btree with in-tree code. This reverses moq-dev/moq#6869's choice and removes it from the "Kept" list in Audit cost. That audit compared it only with another library (@js-sdsl/ordered-map), never with in-tree code. The ledger bounds what a peer can make us do, which is core functionality, and the 150 lines are covered by a differential property test. The alternative was to keep it at about 5 KB Brotli per bundle.
  • Manual moves to @moq/clock/test with no migration note. @moq/clock is unreleased (0.1.0; its MIGRATE.md says "First release"), so no published consumer breaks.
  • qmux stays in the first load. It is the next largest removable piece, 44 KB raw in js/net. The WebSocket fallback races every connect after a short head start, and it is the only transport where WebTransport is missing. A lazy chunk would either be fetched on nearly every connect or add a round trip to the connect itself.
  • pako stays. It was already in the first load on 10-22, so it isn't part of the growth. Its chunked inflate (chunkSize: 4096) stops a decompression bomb at the declared size. Smaller libraries such as fflate inflate a whole push before the callback runs, which would lose that bound.

Impact

  • @moq/clock (unreleased): Manual is exported from @moq/clock/test, not @moq/clock. @moq/net's Clock namespace no longer includes Manual. Migration: import { Manual } from "@moq/clock/test".
  • @moq/net drops its sorted-btree dependency. Its public API is unchanged.
  • No wire change.

Validation

  • bun sh/size/bundles.ts on builds of main and this branch gave the table above (same host and toolchain).

  • js/net/src/util/sorted.test.ts checks Sorted against a plain sorted array: 100 fast-check runs of up to 4,000 mixed inserts and deletes (size: "max", so chunks split and merge), comparing every lookup and every iteration. It also passes with the chunk size temporarily forced to 2, where nearly every operation splits or merges.

  • Ledger A/B, with the old sorted-btree ledger and the new one interleaved in one process (5 rounds, median ns per arrival, including expiry and coverage). Headless Chrome, which is the target:

    Arrivals Order sorted-btree Sorted
    2,048 ascending 439 293
    2,048 descending 586 781
    2,048 shuffled 830 928
    2,048 rolling 684 610
    32,768 ascending 592 504
    32,768 descending 870 1,010
    32,768 shuffled 1,212 1,324
    32,768 rolling 902 865

    In-order and rolling arrivals (the normal cases) are faster. Reordered arrivals are 8 to 33% slower, about 1 µs per arrival even with 32k open gaps. Bun showed parity within noise. The host was loaded, so treat these as indicative.

  • The built @moq/net Ledger constructs and accounts under plain Node ESM. The old CommonJS-only dependency needed a shim for that.

  • The built @moq/clock exports Manual only from ./test.

  • just check passes, including every JS package's tests (@moq/net 3,266, @moq/watch 628, @moq/hang 551, ...).

Caused by

  • moq-dev/moq#6869 - took a 19 KB dependency for the ledger's ordered indexes.
  • moq-dev/moq#5538 - exported the test-only Manual clock from the package index, so it ships in every bundle that re-exports @moq/clock.

Follow-ups

None.

Models

Size: M
Implementer: Claude Code, claude-opus-5-5

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

Review

Rewritten by quest review after each round.

  • Head: ae8616b

  • Base: origin/main at d9f3ceb

  • Reviewer: Codex, gpt-6.1-sol, high effort

  • Session: 01a1d6f2-b98e-7383-bca9-a6fc6080c2db, round 1

  • Score: 9/10, No actionable regressions were found: the ordered-index replacement preserves ledger behavior and the Manual export move is consistent, though builds and tests were not run as instructed.

  • Rounds: 1; findings 0: fixed 0, refuted 0, open 0

  • Findings: none

kixelated and others added 3 commits November 13, 2026 13:47
The ledger's two ordered indexes use an in-tree chunked Sorted list instead
of sorted-btree, and @moq/clock's test-only Manual clock moves to
@moq/clock/test, so no shipped bundle carries either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title The JS bundles shed what isn't worth its weight perf(js): drop sorted-btree and the Manual clock from every bundle Nov 13, 2026
@kixelated
kixelated marked this pull request as ready for review November 13, 2026 22:07
@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Walkthrough

The clock package now exposes Manual through @moq/clock/test, and documentation, benchmarks, tests, and other consumers use that entry point. The network ledger replaces sorted-btree with a new in-tree chunked Sorted collection. New tests cover the collection’s ordered operations and deletion behavior. The dependency list and audit documentation also record the replacement.

Merge Risk: Minimal · up to ae861

This PR moves the test-only Manual clock to @moq/clock/test and replaces sorted-btree with an in-tree sorted list in the ledger to shrink bundles. No in-repo consumer is left on the old import path, and the repository allows breaking API changes on main. Correct the PR note that says @moq/net has no public API change, because Clock.Manual is no longer exported there. Otherwise the change looks safe to merge.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names two changes: dropping the sorted-btree dependency and moving the Manual test clock out of every bundle. The changeset does both. The ledger now uses an in-tree Sorted class in place of…
Description check Passed The description matches the changeset. It covers replacing sorted-btree with the in-tree Sorted list in js/net/src/util/sorted.ts, moving Manual to @moq/clock/test, the bundle-size tables, the Nightly…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names two changes: dropping the sorted-btree dependency and moving the Manual test clock out of every bundle. The changeset does both. The ledger now uses an in-tree Sorted class in place of sorted-btree, and Manual moves from @moq/clock to @moq/clock/test. The title is specific and matches the main changes, and it follows the conventional commit style.

Full details: Description check

Explanation

The description matches the changeset. It covers replacing sorted-btree with the in-tree Sorted list in js/net/src/util/sorted.ts, moving Manual to @moq/clock/test, the bundle-size tables, the Nightly size-check impact, and the validation steps. It is detailed and on topic.

Finishing Touches

Simplify code

  • Commit to this branch
  • Create a new PR
  • 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional comments: 1

js/clock/src/index.ts-12-12: 1

12-12: Major · Data Integrity & Integration · Heavy lift

Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.

Preserve or declare the public API break.

Removing this export breaks existing import { Manual } from "@moq/clock" imports. It also removes Clock.Manual from @moq/net, contrary to the stated unchanged public API. Preserve compatibility or release this as a breaking change with migration guidance.

Review info

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: c1c5c030-e3e7-4cd6-8eb2-8132c335fce9
Commits

Reviewing files that changed from the base of the PR and between d6195e8 and ae8616b.

Files ignored due to path filters: 1

  • bun.lock is excluded by !**/*.lock
Files selected for processing: 100

  • doc/lib/js/clock.md
  • js/auth/src/key.test.ts
  • js/clock/README.md
  • js/clock/api/index.api.md
  • js/clock/api/test.api.md
  • js/clock/bench/timers.ts
  • js/clock/src/index.test.ts
  • js/clock/src/index.ts
  • js/clock/src/test.ts
  • js/hang/src/container/consumer.backpressure.test.ts
  • js/hang/src/container/consumer.holes.test.ts
  • js/hang/src/container/consumer.nonsequential.test.ts
  • js/hang/src/container/consumer.outoforder.test.ts
  • js/hang/src/container/consumer.rewind.test.ts
  • js/hang/src/container/consumer.startup.test.ts
  • js/hang/src/container/consumer.test.ts
  • js/hang/src/container/consumer.warmup.test.ts
  • js/json/src/snapshot/snapshot.test.ts
  • js/net/bench/drift.ts
  • js/net/bench/forward.ts
  • js/net/bench/refresh.ts
  • js/net/bench/replay.ts
  • js/net/bench/resume-history.ts
  • js/net/package.json
  • js/net/src/auth.test.ts
  • js/net/src/broadcast.test.ts
  • js/net/src/cache.test.ts
  • js/net/src/connection/bandwidth.test.ts
  • js/net/src/connection/close.test.ts
  • js/net/src/connection/connect.test.ts
  • js/net/src/connection/forward.test.ts
  • js/net/src/connection/migrate.test.ts
  • js/net/src/connection/pool.test.ts
  • js/net/src/connection/reload.test.ts
  • js/net/src/connection/stats.test.ts
  • js/net/src/connection/ticker.test.ts
  • js/net/src/fill.test.ts
  • js/net/src/goaway-requests.test.ts
  • js/net/src/group.test.ts
  • js/net/src/ietf/aliases.test.ts
  • js/net/src/ietf/join.test.ts
  • js/net/src/ietf/malformed.test.ts
  • js/net/src/ietf/publisher.test.ts
  • js/net/src/ietf/request_stream.test.ts
  • js/net/src/ietf/subscriber.test.ts
  • js/net/src/ietf/tail.test.ts
  • js/net/src/integration.test.ts
  • js/net/src/ledger.ts
  • js/net/src/lite/compression.test.ts
  • js/net/src/lite/connection.test.ts
  • js/net/src/lite/copy_fill.test.ts
  • js/net/src/lite/drop_record.test.ts
  • js/net/src/lite/publisher.test.ts
  • js/net/src/lite/refresh.test.ts
  • js/net/src/lite/subscriber.test.ts
  • js/net/src/lite/tail.test.ts
  • js/net/src/mock.ts
  • js/net/src/net-cleanups.test.ts
  • js/net/src/open.test.ts
  • js/net/src/parked-expiry.test.ts
  • js/net/src/request-deadline.test.ts
  • js/net/src/resume.model.test.ts
  • js/net/src/resume.test.ts
  • js/net/src/socket.test.ts
  • js/net/src/subscribe_priority.test.ts
  • js/net/src/tail.test.ts
  • js/net/src/track.test.ts
  • js/net/src/util/sorted.test.ts
  • js/net/src/util/sorted.ts
  • js/net/src/util/timeout.test.ts
  • js/p2p/src/transport.test.ts
  • js/publish/bench/audio-page.ts
  • js/publish/bench/catalog.ts
  • js/publish/src/audio/encoder.test.ts
  • js/publish/src/broadcast.test.ts
  • js/publish/src/catalog.test.ts
  • js/publish/src/source/file.test.ts
  • js/publish/src/source/pace.test.ts
  • js/publish/src/source/retry.test.ts
  • js/publish/src/video/capture.test.ts
  • js/publish/src/video/encoder-stall.test.ts
  • js/publish/src/video/encoder.test.ts
  • js/qmux/src/session.test.ts
  • js/qmux/src/web-socket-stream.test.ts
  • js/room/src/chat.test.ts
  • js/signals/src/index.test.ts
  • js/watch/src/audio/decoder.test.ts
  • js/watch/src/audio/playout.test.ts
  • js/watch/src/audio/replay.ts
  • js/watch/src/echo.test.ts
  • js/watch/src/feedback.test.ts
  • js/watch/src/retention.test.ts
  • js/watch/src/sync.test.ts
  • js/watch/src/video/decoder.test.ts
  • quest/m0/nightly/bundle-size.md
  • quest/m3/audit-cost.md
  • test/audio-quality/clients/js/src/capture.test.ts
  • test/audio-quality/clients/js/src/probe.test.ts
  • test/interop/clients/js/net-cleanups/main.ts
  • test/relay-fill/relay.ts
Files with no reviewable changes: 2

  • js/clock/api/index.api.md
  • quest/m0/nightly/bundle-size.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

I reviewed the whole PR: the in-tree Sorted list that replaces sorted-btree in the ledger, and the move of the Manual clock to @moq/clock/test across every importer.

  • Sorted chunked list: js/net/src/util/sorted.ts keeps values in sorted chunks of 128 to 512 entries. Lookups use two binary searches, and #seek keeps its result in fields, so a lookup allocates nothing. I traced #seek, #before, the split in insert, and the merge and re-halve in delete. In each, a position before the last chunk never lands past that chunk's end, and after the first split every chunk keeps at least 128 entries.

  • Ledger port: js/net/src/ledger.ts maps each sorted-btree call to its Sorted equivalent:

    • getPairOrNextLower → floor
    • getPairOrNextHigher → ceil
    • nextHigherPair → higher
    • nextLowerPair → lower
    • entries → from

    #nextExpiry now matches runs by identity instead of by start. The two are equivalent because both indexes hold the same Run objects. demand changes only #ages while it iterates #runs, which follows Sorted's rule against changing a list during iteration. The ledger never inserts a key that is already stored.

  • sorted-btree removed: it is gone from js/net/package.json and bun.lock, and so is the CommonJS default-import shim.

  • Manual moved to @moq/clock/test: the move covers the index, API reports, docs, README, benches, mock.ts, replay.ts, and the harnesses under test/. No import of Manual from the @moq/clock root remains, and nothing uses Clock.Manual through @moq/net.

  • Quest bookkeeping: quest/m0/nightly/bundle-size.md is completed, and quest/m3/audit-cost.md now lists sorted-btree as replaced.

I ran bun test src/util/sorted.test.ts src/ledger* in js/net locally: 15 pass, 0 fail.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 2af9386 Nov 14, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m0/nightly/bundle-size branch November 14, 2026 02:50
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