Skip to content

perf(pm): minimum-viable preload bundle (worker-pool + OnceMap + aws-lc-rs) - #2838

Closed
elrrrrrrr wants to merge 8 commits into
nextfrom
perf/pm-perf-bundle
Closed

elrrrrrrr wants to merge 8 commits into
nextfrom
perf/pm-perf-bundle

Conversation

@elrrrrrrr

Copy link
Copy Markdown
Contributor

Summary

The minimum subset of #2818's 101 commits that, individually validated, capture the perf-driver synergy:

  1. `feat(ruborist): add OnceMap util` — copy oncemap.rs from pm/util to ruborist/util so the resolver can use it (no usage yet).
  2. `perf(ruborist): rustls with aws-lc-rs` — drop `ring` for BoringSSL primitives. TLS handshake CPU 162ms → 46ms p50 per measurement in perf(pm): sequential writes within tarball extraction #2818's commit msg.
  3. `perf(ruborist): OnceMap dedup for concurrent manifest fetches` — wire OnceMap into UnifiedRegistry::resolve_full_manifest. When 50 packages all transitively pull `react`, only the first hits network; the rest await the shared Arc.
  4. `perf(pm): preload multi-thread worker pool (tokio::spawn)` — N tokio::spawn workers + SegQueue + MaybeSend/MaybeSync shim for wasm compat.

4 commits, ~7 files. No tarball-extract / cap-raise / DNS-tuning / etc. from #2818.

Driver-hunt history

PR scope npmjs p1_resolve takeaway
#2832 mt-pool only 4.59s ±1.66 mean drop, σ huge
#2835 aws-lc-rs only 6.13s ±1.00 nop
#2834 all 101 commits 2.62s ±0.07 -52%
#2836 #2834 minus wp 4.27s ±0.05 wp ≈ -30% of gain
#2837 #2836 minus OnceMap 5.95s ±1.46 OnceMap ≈ stability
this minimum-viable bundle TBD aiming for #2834-level

The synergy hypothesis: aws-lc-rs unblocks TLS-CPU bottleneck → OnceMap removes duplicate fetches → worker-pool's N independent task slots finally have unique work to do. None alone shows the win.

Local data

ant-design `utoo deps` cold (mac, system proxy on — absolute numbers polluted but ratio is signal):

binary wall
origin/next baseline 2m19s
this bundle 11s

12.6× ratio matches the order-of-magnitude expected from TLS 3.5× + OnceMap dedup ~2× + parse parallelism. CI bench-phases on clean network is the real signal.

Test plan

  • cargo fmt + clippy + test pass
  • cargo build --release pass
  • ant-design `utoo deps` macOS: exit 0, no silent crash
  • CI `pm-e2e-*` (4 platforms; linux is the silent-crash smoke from perf(pm): preload worker-pool replaces FuturesUnordered #2827)
  • CI `utooweb-ci-build-wasm` (validates MaybeSend shim)
  • CI `pm-bench-phases-*` head-to-head vs `utoo-next`

🤖 Generated with Claude Code

elrrrrrrr and others added 4 commits April 27, 2026 21:04
Copy the OnceMap util module from pm/util to ruborist/util so
the resolver can use it for per-name dedup of concurrent
manifest fetches. Foundation commit — no usage yet, that lands
in the worker-pool wiring commit.

The pm-side oncemap.rs stays for cloner/downloader users.
CI pcap analysis against npmjs.org revealed TLS CPU as a major
preload bottleneck. Per-handshake timing on CI:

                          utoo (ring)    bun (BoringSSL)
  CH → SH (1 RTT)           11 ms          10 ms    (network)
  CCS → first AppData       78 ms p50      12 ms p50
                           154 ms max      17 ms max
  TOTAL CH → AppData       162 ms         46 ms

The "CCS → AppData" phase is dominated by post-ChangeCipherSpec
client work (Finished MAC verify, state machine transition,
request dispatch). Observed at CI pcap capture:

  utoo: Client CCS spread 154ms (first conn done at 0.975s, last
        at 1.129s), then first AppData *all* fire within 11ms at
        ~1.13s — classic CPU-saturation pattern where 128 parallel
        TLS handshakes serialise across 4 blocking threads and HTTP
        dispatch is starved until crypto drains.
  bun:  CCS spread 51ms, AppData spread 43ms — dispatch flows
        smoothly as each conn completes.

`ring` (rustls' default provider) is pure Rust + hand-tuned
assembly; `aws-lc-rs` wraps BoringSSL's primitives which are more
aggressively optimised for x86_64 AES-NI + SHA-NI.

Reqwest 0.12's `rustls-tls-native-roots` feature pins `__rustls-ring`
via Cargo's feature unification — no way to override. Swap to
`rustls-tls-native-roots-no-provider`, add a direct `rustls` dep
with the `aws-lc-rs` feature, load native root certs via
`rustls-native-certs` and pass the `ClientConfig` into reqwest via
`use_preconfigured_tls`.

Local M2 (ARM, where ring's hand-tuned ARM assembly is already
near-optimal) shows neutral perf (~2.9s both providers). Waiting
on CI to confirm the x86_64 CCS→AppData gap closes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the OnceMap from #2832-foundation into UnifiedRegistry's
resolve_full_manifest. When N packages all transitively pull
on the same dep (e.g. 50 packages → react), only the first
hits the network; the others await the shared Arc<result>
on Notify and clone it on resolve.

DashMap + tokio::sync::Notify keeps the fast path (cache hit)
lock-free. cfg-gated on native; wasm path keeps the direct
fetch since dashmap + tokio sync are unnecessary there.

FullManifestResult derives Clone so OnceMap waiters can each
take owned copies.
N long-lived `tokio::spawn` workers pull work from a shared
lock-free `SegQueue`. Each worker is independent on tokio's
multi-thread runtime, so when one worker is parsing manifest
JSON (CPU-bound, simd_json), other workers continue driving
their network IO and other parses run on different cores.
Replaces `FuturesUnordered` where the main task owned all
preload futures and polled them cooperatively — every per-
future await continuation (including parse) ran serialised
in that single task.

The architectural fix alone (proven nop in PR #2830 with
spawn_local) needs the network-layer drivers (aws-lc-rs +
OnceMap, earlier in this stack) to actually express its
benefit. With them: TLS handshake CPU is fast (aws-lc-rs),
duplicate fetches are deduped (OnceMap), so worker-pool's
N independent task slots are doing actual unique work.

## MaybeSend / MaybeSync shim

`tokio::spawn` requires `Future: Send + 'static`, which
forces `RegistryClient` trait surface to grow `+ Send` on
its `impl Future` returns + `Self: Sync` / `Self::Error:
Send` on default impls. Native uses real Send/Sync; wasm32
uses no-op marker traits because `JsFuture` is `!Send` and
tasks run via `wasm_bindgen_futures::spawn_local`.

`PreloadRegistry` trait alias bundles `RegistryClient +
Clone + MaybeSend + MaybeSync + 'static` so the 5 resolver
entry points (`build_deps*`, `resolve*`, `run_preload_phase`)
read clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@elrrrrrrr elrrrrrrr added benchmark Run pm-bench on PR bench-phases Run pm-bench-phases workflow labels Apr 27, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the manifest preloading system to use a multi-threaded worker pool with WASM support and introduces a OnceMap utility to deduplicate concurrent network requests. It also migrates the TLS implementation to aws-lc-rs for improved performance. Feedback identifies a race condition in the worker termination logic, the use of unstable let_chains syntax which will break stable Rust compilation, and an inefficient method for calculating processed statistics. Additionally, it is recommended to narrow the scope of the dead_code allowance.

Comment on lines +191 to +196
pending.push(tdep);
count += 1;
}
}
if count > 0 {
dispatched.fetch_add(count, Ordering::Release);

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.

high

There is a race condition in the worker termination logic. The dispatched counter must be incremented before pushing new work items to the pending queue. If pending.push happens first, another worker could pop the item, finish it, and increment completed before the current worker increments dispatched. This would cause completed == dispatched to be true prematurely, leading to early shutdown of the preload phase.

                    let new_added = if let Ok(resolved) = &result {
                        let mut to_push = Vec::new();
                        for tdep in extract_transitive_deps(&resolved.manifest, &config_for_worker) {
                            let key = format!("{}@{}", tdep.0, tdep.1);
                            if processed.insert(key) {
                                to_push.push(tdep);
                            }
                        }
                        let count = to_push.len();
                        if count > 0 {
                            dispatched.fetch_add(count, Ordering::Release);
                            for tdep in to_push {
                                pending.push(tdep);
                            }
                            // Wake parked workers so they pick up the new
                            // work before checking the termination condition.
                            notify.notify_waiters();
                        }
                        count
                    } else {
                        0
                    };

Comment on lines +128 to +132
if let Some(entry) = self.map.get(key)
&& matches!(entry.value(), Value::Done(_))
{
return;
}

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.

high

The use of let_chains (e.g., if let ... && ...) is an unstable Rust feature. Unless this project explicitly targets nightly and enables the feature flag, this will cause a compilation error on stable Rust. It's safer to use nested if let blocks.

Suggested change
if let Some(entry) = self.map.get(key)
&& matches!(entry.value(), Value::Done(_))
{
return;
}
if let Some(entry) = self.map.get(key) {
if matches!(entry.value(), Value::Done(_)) {
return;
}
}

Comment on lines +235 to +239
if let Some(entry) = self.map.get(&key)
&& let Value::Done(result) = entry.value()
{
return Some(Arc::clone(result));
}

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.

high

The use of let_chains is unstable and will break compilation on stable Rust. Please use nested if let instead.

Suggested change
if let Some(entry) = self.map.get(&key)
&& let Value::Done(result) = entry.value()
{
return Some(Arc::clone(result));
}
if let Some(entry) = self.map.get(&key) {
if let Value::Done(result) = entry.value() {
return Some(Arc::clone(result));
}
}

Comment on lines +301 to +307
stats.total_processed = {
let mut set: HashSet<String> = HashSet::with_capacity(processed.len());
for entry in processed.iter() {
set.insert(entry.key().clone());
}
set.len()
};

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.

medium

Calculating total_processed by cloning the entire DashSet into a new HashSet is highly inefficient (O(N) time and space). DashSet::len() provides the count by summing shard lengths without allocating or copying the underlying data.

    stats.total_processed = processed.len();

@@ -0,0 +1,465 @@
#![allow(dead_code)]

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.

medium

The #![allow(dead_code)] attribute at the crate level is too broad. It's better to allow it only on specific functions that are intended for future use, or remove it entirely if this is meant to be a public API.

aws-lc-sys cc_builder.rs:692 panics on wasm32 target (no
support). Move the explicit rustls + aws-lc-rs dependency to
native-only; reqwest's rustls-tls-native-roots-no-provider
feature on wasm is a no-op since wasm uses the fetch API and
the browser's TLS stack, not rustls.
@github-actions

Copy link
Copy Markdown

📊 pm-bench-phases · 33388e0 · linux (ubuntu-latest)

Workflow run — ant-design

PMs: utoo (this branch) · utoo-npm (latest published) · bun (latest)

npmjs.org

p0_full_cold

PM wall ±σ user sys RSS pgMinor
bun 9.40s 0.45s 10.09s 10.15s 648M 315.8K
utoo-npm 10.11s 0.39s 11.60s 13.24s 1.32G 155.8K
utoo 9.53s 0.90s 12.47s 13.04s 1.33G 156.5K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 14.7K 17.0K 1.16G 6M 1.83G 1.72G 1M
utoo-npm 165.6K 153.0K 1.14G 4M 1.68G 1.68G 2M
utoo 131.9K 95.9K 1.14G 5M 1.68G 1.68G 2M

p1_resolve

PM wall ±σ user sys RSS pgMinor
bun 2.17s 0.05s 3.91s 1.07s 502M 171.9K
utoo-npm 5.26s 0.06s 5.92s 1.19s 429M 67.9K
utoo 3.03s 0.11s 6.62s 1.95s 569M 83.1K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 9.7K 3.9K 200M 3M 104M - 1M
utoo-npm 65.3K 3.0K 202M 2M 9M 5M 2M
utoo 39.4K 20.7K 198M 2M 9M 5M 2M

p3_cold_install

PM wall ±σ user sys RSS pgMinor
bun 7.18s 0.77s 6.09s 9.88s 588M 197.5K
utoo-npm 7.29s 1.26s 5.57s 11.33s 991M 105.7K
utoo 6.40s 0.07s 5.47s 11.27s 913M 125.1K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 5.1K 6.9K 993M 4M 1.73G 1.73G 1M
utoo-npm 109.7K 72.0K 964M 3M 1.67G 1.67G 2M
utoo 104.0K 79.3K 964M 3M 1.67G 1.67G 2M

p4_warm_link

PM wall ±σ user sys RSS pgMinor
bun 3.29s 0.16s 0.22s 2.34s 135M 32.5K
utoo-npm 2.58s 0.19s 0.65s 4.00s 82M 18.7K
utoo 2.37s 0.06s 0.65s 4.01s 82M 18.4K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 272 23 7M 52K 1.88G 1.72G 1M
utoo-npm 50.0K 21.5K 21K 10K 1.67G 1.67G 2M
utoo 50.7K 22.8K 21K 10K 1.68G 1.67G 2M

npmmirror.com

p0_full_cold

PM wall ±σ user sys RSS pgMinor
bun 35.12s 11.91s 9.22s 9.56s 532M 381.8K
utoo-npm 26.95s 5.05s 8.66s 14.38s 776M 98.1K
utoo 51.32s 32.76s 8.58s 14.28s 782M 105.1K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 53.8K 4.6K 1.12G 9M 1.84G 1.72G 2M
utoo-npm 249.2K 107.1K 978M 8M 1.67G 1.68G 2M
utoo 236.8K 86.5K 981M 8M 1.67G 1.68G 2M

p1_resolve

PM wall ±σ user sys RSS pgMinor
bun 1.48s 0.05s 3.97s 1.09s 607M 186.5K
utoo-npm 5.23s 0.11s 2.10s 0.59s 75M 16.1K
utoo 3.22s 1.83s 1.50s 0.54s 82M 17.3K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 5.0K 6.1K 151M 2M 106M - 2M
utoo-npm 45.9K 751 12M 2M - 4M 2M
utoo 26.6K 1.8K 16M 2M - 4M 2M

p3_cold_install

PM wall ±σ user sys RSS pgMinor
bun 23.97s 1.28s 5.43s 8.24s 243M 113.2K
utoo-npm 46.43s 30.54s 6.29s 12.68s 585M 89.0K
utoo 51.63s 30.07s 6.05s 12.89s 582M 92.6K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 32.7K 2.3K 984M 7M 1.70G 1.70G 2M
utoo-npm 190.7K 93.8K 965M 6M 1.67G 1.67G 2M
utoo 194.9K 94.6K 975M 6M 1.67G 1.67G 2M

p4_warm_link

PM wall ±σ user sys RSS pgMinor
bun 15.01s 20.24s 0.18s 2.25s 134M 30.8K
utoo-npm 2.47s 0.26s 0.62s 3.86s 82M 18.9K
utoo 2.27s 0.09s 0.63s 3.90s 83M 19.4K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 633 22 3M 118K 1.82G 1.72G 2M
utoo-npm 48.1K 21.9K 86K 15K 1.67G 1.67G 2M
utoo 49.6K 22.0K 85K 13K 1.67G 1.67G 2M

@github-actions

Copy link
Copy Markdown

📊 pm-bench-phases · 33388e0 · mac (macos-latest)

Workflow run — ant-design

PMs: utoo (this branch) · utoo-npm (latest published) · bun (latest)

npmjs.org

p0_full_cold

PM wall ±σ user sys RSS pgMinor
bun 17.54s 1.87s 6.20s 17.47s 774M 50.0K
utoo-npm 22.63s 6.56s 10.45s 23.24s 933M 96.9K
utoo 17.70s 3.92s 9.43s 20.66s 1.10G 109.9K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 16.1K 143.9K - - 1.79G 1.90G 1M
utoo-npm 13.9K 360.2K - - 1.63G 1.88G 2M
utoo 7.2K 352.2K - - 1.63G 1.87G 2M

p1_resolve

PM wall ±σ user sys RSS pgMinor
bun 2.97s 0.12s 3.13s 1.66s 475M 30.8K
utoo-npm 5.71s 0.29s 4.50s 2.70s 549M 37.2K
utoo 5.43s 1.91s 5.26s 3.03s 609M 40.5K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 34 21.6K - - 110M - 1M
utoo-npm 14 72.6K - - 28M 5M 2M
utoo 12 81.1K - - 28M 5M 2M

p3_cold_install

PM wall ±σ user sys RSS pgMinor
bun 15.83s 5.17s 3.28s 15.01s 517M 33.7K
utoo-npm 14.65s 4.65s 3.78s 15.70s 791M 83.1K
utoo 12.77s 2.78s 3.84s 16.47s 747M 80.7K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 5.6K 132.3K - - 1.70G 1.94G 1M
utoo-npm 1.6K 238.7K - - 1.60G 1.83G 2M
utoo 1.4K 220.3K - - 1.60G 1.83G 2M

p4_warm_link

PM wall ±σ user sys RSS pgMinor
bun 6.53s 1.56s 0.12s 2.53s 52M 3.9K
utoo-npm 5.53s 0.84s 0.69s 3.77s 97M 7.2K
utoo 3.78s 0.54s 0.51s 2.88s 92M 7.0K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 17.0K 1.0K - - 1.87G 1.91G 1M
utoo-npm 13.0K 76.5K - - 1.60G 1.83G 2M
utoo 13.5K 74.2K - - 1.63G 1.83G 2M

npmmirror.com

p0_full_cold

PM wall ±σ user sys RSS pgMinor
bun 50.39s 36.38s 7.77s 24.95s 598M 38.6K
utoo-npm 51.90s 22.80s 8.93s 26.08s 778M 77.9K
utoo 27.55s 1.13s 7.30s 20.82s 820M 86.0K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 15.4K 151.5K - - 1.78G 1.90G 2M
utoo-npm 4.3K 429.4K - - 1.61G 1.87G 2M
utoo 898 413.5K - - 1.61G 1.86G 2M

p1_resolve

PM wall ±σ user sys RSS pgMinor
bun 16.45s 22.35s 3.13s 1.88s 558M 36.3K
utoo-npm 4.61s 0.05s 1.79s 0.98s 79M 5.8K
utoo 4.20s 3.83s 1.45s 0.66s 83M 6.0K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 36 26.0K - - 111M - 2M
utoo-npm 5 33.6K - - - 4M 2M
utoo 28 17.9K - - - 4M 2M

p3_cold_install

PM wall ±σ user sys RSS pgMinor
bun 34.16s 16.69s 3.61s 15.05s 250M 16.5K
utoo-npm 54.67s 33.01s 5.62s 20.39s 641M 73.5K
utoo 32.00s 3.78s 4.84s 17.24s 673M 74.9K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 1.9K 128.2K - - 1.65G 1.91G 2M
utoo-npm 1.8K 374.4K - - 1.61G 1.87G 2M
utoo 1.6K 369.5K - - 1.61G 1.87G 2M

p4_warm_link

PM wall ±σ user sys RSS pgMinor
bun 5.15s 0.54s 0.08s 2.14s 44M 3.4K
utoo-npm 3.42s 0.10s 0.49s 2.54s 91M 6.8K
utoo 3.75s 0.48s 0.49s 2.76s 87M 6.6K
PM vCtx iCtx netRX netTX cache node_mod lock
bun 13.1K 444 - - 1.75G 1.90G 2M
utoo-npm 12.8K 69.3K - - 1.61G 1.82G 2M
utoo 12.6K 71.5K - - 1.61G 1.82G 2M

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bench-phases Run pm-bench-phases workflow benchmark Run pm-bench on PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant