Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
ad0dee9
perf(pm): sequential writes within tarball extraction
elrrrrrrr Apr 21, 2026
c057eff
ci(pm-bench): robust output + warm skip + json fault tolerance
elrrrrrrr Apr 21, 2026
7ab17b8
perf(pm): chunked parallel writes within tarball extraction
elrrrrrrr Apr 21, 2026
37b1741
perf(pm): surface per-phase cumulative timings in install summary
elrrrrrrr Apr 21, 2026
ef6b5fd
ci(pm-bench): surface cold install stdout via hyperfine --show-output
elrrrrrrr Apr 21, 2026
b4d3c11
ci(pm-bench-phases): phase-isolated cold install bench
elrrrrrrr Apr 22, 2026
d021c23
ci(pm-bench-phases): trigger on PR label so it runs before default-br…
elrrrrrrr Apr 22, 2026
a914cdb
ci(pm-bench-phases): robust per-iteration prepare via generated scripts
elrrrrrrr Apr 22, 2026
2d6b2a1
ci(pm-bench-phases): use utoo clean / bun pm cache rm for reliable ca…
elrrrrrrr Apr 22, 2026
2e0f800
ci(pm-bench-phases): scope caches to bench dirs via --cache-dir / env
elrrrrrrr Apr 22, 2026
c943bd7
debug(pm-bench-phases): log prepare script + before/after cache sizes
elrrrrrrr Apr 22, 2026
d6f1ae3
fix(pm-bench-phases): match full phase name in case patterns
elrrrrrrr Apr 22, 2026
6c6f116
ci(pm-bench-phases): drop debug wrapper now that prepare is verified
elrrrrrrr Apr 22, 2026
87579e0
feat(pm-bench-phases): collect RSS / ctx / IO per phase via /usr/bin/…
elrrrrrrr Apr 22, 2026
76cc6ef
feat(pm-bench-phases): capture detailed IO, network, CPU per phase
elrrrrrrr Apr 22, 2026
d16f338
fix(pm-bench-phases): track disk writes by path, drop noisy pgpg counter
elrrrrrrr Apr 23, 2026
7c1ebbf
fix(pm-bench-phases): move du out of timed window
elrrrrrrr Apr 23, 2026
99780af
fix(pm-bench-phases): skip footprint JSON when loading hyperfine results
elrrrrrrr Apr 23, 2026
a8743b3
perf(ruborist): enable gzip + brotli on manifest HTTP client
elrrrrrrr Apr 23, 2026
51b5ede
perf(ruborist): use HTTP/1.1 + connection pool for manifest fetching
elrrrrrrr Apr 23, 2026
361c5d3
Revert "perf(ruborist): use HTTP/1.1 + connection pool for manifest f…
elrrrrrrr Apr 23, 2026
14f2777
debug(ruborist): per-request latency histogram for manifest fetches
elrrrrrrr Apr 23, 2026
50a7014
debug(ruborist): switch histogram output to stdout (survives metrics …
elrrrrrrr Apr 23, 2026
6afc0c8
perf(ruborist): use HTTP/1.1 + connection pool for manifest fetching
elrrrrrrr Apr 23, 2026
4399011
perf(ruborist): dedup concurrent full-manifest fetches
elrrrrrrr Apr 23, 2026
4f238b4
chore(ruborist): remove debug histogram instrumentation
elrrrrrrr Apr 23, 2026
ee987ef
ci(pm-bench-phases): add p0_full_cold phase
elrrrrrrr Apr 23, 2026
608db9f
perf(ruborist): round-robin DNS addresses across connection pool
elrrrrrrr Apr 23, 2026
1f74f9a
ci(pm-bench-phases): capture pcap of resolve phase for investigation
elrrrrrrr Apr 23, 2026
99709b7
perf(ruborist): raise manifest fetch concurrency to 256
elrrrrrrr Apr 23, 2026
579aedc
perf(ruborist): preheat HTTP connection pool before resolve phase
elrrrrrrr Apr 23, 2026
cc5f63c
perf(ruborist): offload transitive-dep extraction to blocking pool
elrrrrrrr Apr 23, 2026
a104c66
perf(ruborist): offload manifest JSON parse to blocking pool, drop pr…
elrrrrrrr Apr 23, 2026
312f96a
perf(ruborist): fire-and-forget disk cache writes
elrrrrrrr Apr 23, 2026
2117947
perf(pm): raise manifests-concurrency-limit default from 64 to 256
elrrrrrrr Apr 23, 2026
9fc033f
Revert "perf(pm): raise manifests-concurrency-limit default from 64 t…
elrrrrrrr Apr 23, 2026
37700f6
perf(ruborist): multi-client HTTP pool + real preheat
elrrrrrrr Apr 23, 2026
158b46c
perf(ruborist): halve client count and preheat footprint
elrrrrrrr Apr 23, 2026
8da39aa
perf(ruborist): OnceMap dedup + proc_us instrumentation, drop multi-c…
elrrrrrrr Apr 23, 2026
351051d
debug(ruborist): per-request send/body/parse histograms
elrrrrrrr Apr 23, 2026
1f3b38f
debug(ruborist): instrument get_core_version latency
elrrrrrrr Apr 23, 2026
01af1f3
perf(ruborist): eager-parse FullManifest.versions into map lookup
elrrrrrrr Apr 23, 2026
be26ef0
perf(ruborist): drop spawn_blocking wraps from preload hot path
elrrrrrrr Apr 23, 2026
705edae
debug(ruborist): in_flight saturation histogram
elrrrrrrr Apr 23, 2026
392fbf7
perf(pm): raise manifests-concurrency-limit from 64 to 128
elrrrrrrr Apr 23, 2026
7717f13
perf(ruborist): drop redundant skip_on_error from CoreVersionManifest
elrrrrrrr Apr 23, 2026
e1341da
fix(ruborist): restore skip_on_error on non-Value fields
elrrrrrrr Apr 23, 2026
66dad75
perf(ruborist): keep preload HTTP connections hot
elrrrrrrr Apr 23, 2026
c5cd831
perf(ruborist): lazy per-version CoreVersionManifest conversion
elrrrrrrr Apr 24, 2026
fe8365d
Revert "perf(ruborist): lazy per-version CoreVersionManifest conversion"
elrrrrrrr Apr 24, 2026
4c290e2
chore(ruborist): remove preload/fetch latency instrumentation
elrrrrrrr Apr 24, 2026
01f25a8
perf(ruborist): rotate DNS addresses per-family
elrrrrrrr Apr 24, 2026
88d449d
debug(ruborist): resolve_pkg / disk_precheck timers
elrrrrrrr Apr 24, 2026
46cb803
perf(ruborist): skip per-package disk ETag probe on preload
elrrrrrrr Apr 24, 2026
aca2c33
debug(ruborist): full per-future timing breakdown
elrrrrrrr Apr 24, 2026
ff23b09
perf(ruborist): lock-free pending queue for preload
elrrrrrrr Apr 24, 2026
54ce551
chore(ruborist): remove preload timing instrumentation
elrrrrrrr Apr 24, 2026
a89918b
perf(pm): cap tokio blocking pool at worker_threads
elrrrrrrr Apr 24, 2026
82bed6e
perf(ruborist): lazy per-version CoreVersionManifest via simd_json Ow…
elrrrrrrr Apr 24, 2026
97bef6c
perf(pm): drop manifests-concurrency-limit 128 → 64
elrrrrrrr Apr 24, 2026
f24de16
Revert "perf(pm): drop manifests-concurrency-limit 128 → 64"
elrrrrrrr Apr 24, 2026
2750ed9
debug(ruborist): info-level logs for build_deps phase timing
elrrrrrrr Apr 24, 2026
b167c97
perf(ruborist): rustls with aws-lc-rs crypto provider instead of ring
elrrrrrrr Apr 24, 2026
88a4b05
perf(ruborist): restore disk ETag probe via bulk-readdir index
elrrrrrrr Apr 24, 2026
5e3c12d
debug(ruborist): HTTP-only timing diagnostic for preload phase
elrrrrrrr Apr 24, 2026
6e0e60e
debug(ruborist): parse queue-vs-exec timing diagnostic
elrrrrrrr Apr 24, 2026
2581455
perf(pm): raise manifests-concurrency cap from 128 to 256
elrrrrrrr Apr 24, 2026
c610a58
Revert "perf(pm): raise manifests-concurrency cap from 128 to 256"
elrrrrrrr Apr 24, 2026
4e12590
perf(ruborist): per-IP client pool for registry connections
elrrrrrrr Apr 24, 2026
ae2a608
fix(ruborist): prefer IPv4 for pinned client pool
elrrrrrrr Apr 24, 2026
1a16d25
perf(ruborist): enable HTTP/2 negotiation for multi-client pool
elrrrrrrr Apr 24, 2026
f379f7a
Revert "perf(ruborist): enable HTTP/2 negotiation for multi-client pool"
elrrrrrrr Apr 24, 2026
b11dee0
Revert "fix(ruborist): prefer IPv4 for pinned client pool"
elrrrrrrr Apr 24, 2026
5a897e4
Revert "perf(ruborist): per-IP client pool for registry connections"
elrrrrrrr Apr 24, 2026
02ef056
perf(pm): drop manifests-concurrency cap from 128 to 64
elrrrrrrr Apr 25, 2026
b92aa81
chore(manifest-bench): isolated HTTP-only fetch benchmark
elrrrrrrr Apr 25, 2026
17ab84c
perf(pm): tune manifests-concurrency cap from 64 to 96
elrrrrrrr Apr 25, 2026
f6846d6
ci(pm-bench-phases): wire manifest-bench standalone HTTP sweep
elrrrrrrr Apr 25, 2026
f455a0b
perf(pm): drop per-package progress messages from preload hot path
elrrrrrrr Apr 25, 2026
de5c83e
perf(ruborist): drop format!() from preload dedup hot path
elrrrrrrr Apr 25, 2026
90e421a
Revert "perf(ruborist): drop format!() from preload dedup hot path"
elrrrrrrr Apr 25, 2026
2b89d0b
perf(pm): drop indicatif inc()/inc_length() from preload hot path
elrrrrrrr Apr 25, 2026
337d4f2
perf(ruborist): Arc<FullManifest> in memory cache + dedup result
elrrrrrrr Apr 25, 2026
3185736
perf(pm): default file log filter to info+ instead of debug+
elrrrrrrr Apr 25, 2026
bb256ec
perf(ruborist): drop versions.keys.clone() on cached resolve hot path
elrrrrrrr Apr 25, 2026
12d34dd
perf(ruborist): normalize_spec returns Cow, no alloc on common path
elrrrrrrr Apr 25, 2026
ed7b551
perf(ruborist): preload worker-pool replaces FuturesUnordered
elrrrrrrr Apr 25, 2026
b5014bd
perf(pm): raise manifests-concurrency cap 96 → 128 (worker-pool era)
elrrrrrrr Apr 25, 2026
d6fd5f6
perf(pm): manifests-concurrency cap 128 → 160 (worker-pool sweep)
elrrrrrrr Apr 25, 2026
f3f616d
perf(ruborist): inline simd_json parse, drop spawn_blocking
elrrrrrrr Apr 25, 2026
756cf71
perf(pm): cap 160 → 128 (with inline parse, find sweet spot)
elrrrrrrr Apr 25, 2026
bf14995
perf(ruborist): parallelise workspace package.json reads
elrrrrrrr Apr 25, 2026
019ee2f
debug(pm): instrument save_package_lock sub-phases
elrrrrrrr Apr 25, 2026
75b9e20
debug(ruborist): instrument setup phase (workspace + graph init)
elrrrrrrr Apr 25, 2026
1b0c67c
chore(ruborist): drop trailing newline in preload.rs
elrrrrrrr Apr 25, 2026
132ef36
perf(pm): raise tokio max_blocking_threads from N to max(N*4, 32)
elrrrrrrr Apr 25, 2026
9a071f2
Revert "perf(pm): raise tokio max_blocking_threads from N to max(N*4,…
elrrrrrrr Apr 25, 2026
c7c847d
test(pm): drop intra-package rayon par_chunks in tarball extract
elrrrrrrr Apr 25, 2026
2f1092c
Revert "test(pm): drop intra-package rayon par_chunks in tarball extr…
elrrrrrrr Apr 25, 2026
9229e16
perf(pm): clone_dir uses rayon (mirror extractor pattern)
elrrrrrrr Apr 25, 2026
e38329c
Revert "perf(pm): clone_dir uses rayon (mirror extractor pattern)"
elrrrrrrr Apr 25, 2026
1fa0a60
Merge remote-tracking branch 'origin/next' into perf/extract-sequenti…
elrrrrrrr Apr 28, 2026
e23d15f
chore: tombi format Cargo.toml drift after merge
elrrrrrrr Apr 28, 2026
8b11fc7
chore(bench): fix typo unparseable -> unparsable
elrrrrrrr Apr 28, 2026
6ea2549
Merge branch 'next' into perf/extract-sequential-writes
elrrrrrrr Apr 29, 2026
c109e93
fix(ruborist): gate aws-lc-rs path + worker-pool to native targets
elrrrrrrr Apr 29, 2026
da5f783
fix(ruborist): wasm fire-and-forget disk writes via spawn_local
elrrrrrrr Apr 29, 2026
1a3073d
Merge upstream next (PR #2866 ManifestStore decoupling) into PR #2818
elrrrrrrr Apr 29, 2026
7b0997b
fix: restore next.js as submodule gitlink (was committed as symlink)
elrrrrrrr Apr 29, 2026
5a1191a
Merge origin/next: bench script reorg + utoo-next baseline
elrrrrrrr Apr 30, 2026
af714eb
Drop manifest-bench standalone crate + bench script local edits
elrrrrrrr Apr 30, 2026
1110794
chore: drop preload timing instrumentation + restore indicatif progress
elrrrrrrr Apr 30, 2026
26cfeb7
perf(pm): drop indicatif inc()/inc_length() from preload hot path (re…
elrrrrrrr Apr 30, 2026
87031d1
perf(pm): keep only resolved event for indicatif (drop fetching/start…
elrrrrrrr Apr 30, 2026
f122478
perf(pm): gate ProgressReceiver event handling on IsTerminal at startup
elrrrrrrr Apr 30, 2026
5ca65e7
perf(pm): short-circuit version-manifest reparse on sibling-spec hit
elrrrrrrr Apr 30, 2026
b2498e9
perf(pm): drop redundant Vec<String>/HashMap clones on resolve hot path
elrrrrrrr Apr 30, 2026
1a4eb03
Merge origin/next: PR #2876 IS_TTY gate landed upstream
elrrrrrrr Apr 30, 2026
0dee98c
Merge origin/next: PR #2878 VersionsRef view + drop in-memory double-…
elrrrrrrr Apr 30, 2026
5ec6616
chore: drop temporary timing instrumentation residues
elrrrrrrr Apr 30, 2026
9d03d3e
chore: converge to next on cosmetic-only divergences
elrrrrrrr Apr 30, 2026
ae5b0aa
Merge branch 'next' into perf/extract-sequential-writes
elrrrrrrr May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/pm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,15 @@ fn main() {
let result = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(worker_threads)
// Cap the blocking pool to reduce thread-creation thrash on
// runs that burst-dispatch ~128 spawn_blocking calls (JSON
// parse for manifest fetches). Default 512 leaves room for
// unbounded growth; ~32 threads is enough to saturate a 4-10
// core machine on short CPU-bound tasks while keeping the
// pool warm across runs. Observed locally: default pool gave
// bimodal walls (2.7s fast / 6.9s thrash on M2); capping at
// 32 eliminates the thrash peak.
.max_blocking_threads(worker_threads)
.build()
.expect("failed to build tokio runtime")
.block_on(async_main());
Expand Down
49 changes: 33 additions & 16 deletions crates/pm/src/util/extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,24 @@ async fn extract_tarball(gzip_bytes: Bytes, dest: &Path) -> Result<()> {
rx.await.with_context(|| "Extract task panicked")?
}

/// Synchronous extraction: decompress + parse + parallel write, all on rayon.
/// Synchronous extraction: decompress + parse + chunked parallel write, all on rayon.
///
/// Writes are batched into fixed-size chunks: each rayon task writes a contiguous
/// run of `WRITE_CHUNK_SIZE` files sequentially. This keeps multi-core parallelism
/// for IO-overlap while cutting the rayon task count (and its work-stealing
/// futex park/unpark pressure) by the chunk factor. Cross-package parallelism is
/// preserved by the outer `rayon::spawn` in `extract_tarball`.
fn extract_tarball_sync(gzip_bytes: Bytes, estimated_size: usize, dest: &Path) -> Result<()> {
use rayon::prelude::*;
use std::collections::HashSet;
use std::fs;
use std::io::{Cursor, Read, Write};

/// Files per rayon task when writing extracted tar entries. Sized to amortize
/// work-stealing overhead while leaving enough tasks to keep cores busy on
/// large packages (e.g. 500-file packages → ~16 tasks with WRITE_CHUNK_SIZE=32).
const WRITE_CHUNK_SIZE: usize = 32;

// Decompress gzip using libdeflate
let mut output = vec![0u8; estimated_size];

Expand Down Expand Up @@ -152,21 +163,27 @@ fn extract_tarball_sync(gzip_bytes: Bytes, estimated_size: usize, dest: &Path) -
fs::create_dir(dir).ok();
}

// Write files in parallel using rayon
entries.par_iter().try_for_each(|entry| -> Result<()> {
let mut file = fs::File::create(&entry.path)
.with_context(|| format!("Failed to create: {}", entry.path.display()))?;
file.write_all(&entry.content)
.with_context(|| format!("Failed to write: {}", entry.path.display()))?;

// Skip chmod for 0o644 (most files) — File::create() already produces
// this via umask (0o666 & ~0o022 = 0o644).
#[cfg(unix)]
if entry.mode != 0o644 {
fs::set_permissions(&entry.path, fs::Permissions::from_mode(entry.mode)).ok();
}
Ok(())
})?;
// Chunked parallel writes: N files per rayon task. Retains IO-overlap
// parallelism across cores while cutting the rayon task count (and the
// associated work-stealing futex traffic) relative to per-file par_iter.
entries
.par_chunks(WRITE_CHUNK_SIZE)
.try_for_each(|chunk| -> Result<()> {
for entry in chunk {
let mut file = fs::File::create(&entry.path)
.with_context(|| format!("Failed to create: {}", entry.path.display()))?;
file.write_all(&entry.content)
.with_context(|| format!("Failed to write: {}", entry.path.display()))?;

// Skip chmod for 0o644 (most files) — File::create() already
// produces this via umask (0o666 & ~0o022 = 0o644).
#[cfg(unix)]
if entry.mode != 0o644 {
fs::set_permissions(&entry.path, fs::Permissions::from_mode(entry.mode)).ok();
}
}
Ok(())
})?;

// Set directory permissions
#[cfg(unix)]
Expand Down
17 changes: 15 additions & 2 deletions crates/pm/src/util/user_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,22 @@ pub fn get_install_scope() -> InstallScope {
INSTALL_SCOPE.get().copied().unwrap_or_default()
}

// Manifest fetch concurrency configuration
// Manifest fetch concurrency configuration.
//
// Sweep history under the worker-pool architecture:
// cap=96 spawn_blocking parse: wall=2.23s avg_conc=66
// cap=128 spawn_blocking parse: wall=2.15s avg_conc=84
// cap=160 spawn_blocking parse: wall=1.90s avg_conc=81 (parse queue p95=200ms)
// cap=160 inline parse : wall=2.14s avg_conc=119 (parse queue=0ms)
//
// Inline parse eliminated blocking-pool queue but cap=160 lifted
// concurrency past the per-source Cloudflare throttle, inflating
// per-req from 55 ms → 93 ms. Net wall flat. cap=128 + inline parse
// should land near the sweet spot: enough concurrency to saturate
// independent connections, low enough to stay under Cloudflare's
// per-source throttle threshold.
static MANIFESTS_CONCURRENCY_LIMIT: LazyLock<ConfigValue<usize>> =
LazyLock::new(|| ConfigValue::new("manifests-concurrency-limit", 64));
LazyLock::new(|| ConfigValue::new("manifests-concurrency-limit", 128));

pub fn set_manifests_concurrency_limit(value: Option<usize>) {
MANIFESTS_CONCURRENCY_LIMIT.set(value);
Expand Down
49 changes: 25 additions & 24 deletions crates/ruborist/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,47 @@ license = "MIT"

# tombi: format.rules.table-keys-order.disabled = true
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
bytes = "1"
dashmap = "6.1.0"
pathdiff = { workspace = true }
deno_semver = "0.7"
dirs = { workspace = true }
futures = "0.3"
parking_lot = { workspace = true }
petgraph = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = { workspace = true }
simd-json = "0.17"
thiserror = { workspace = true }
tokio-fs-ext = { workspace = true }
tokio-retry = "0.3"
tracing = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
bytes = "1"
crossbeam-queue = "0.3"
dashmap = "6.1.0"
pathdiff = { workspace = true }
deno_semver = "0.7"
dirs = { workspace = true }
futures = "0.3"
parking_lot = { workspace = true }
petgraph = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = { workspace = true }
simd-json = "0.17"
thiserror = { workspace = true }
tokio-fs-ext = { workspace = true }
tokio-retry = "0.3"
tracing = { workspace = true }
# TLS provider is selected per-target below: macOS overrides to
# aws-lc-rs (see service/http.rs); every other target keeps ring.
reqwest = { version = "0.12", default-features = false, features = ["http2", "json", "socks"] }
reqwest = { version = "0.12", default-features = false, features = ["http2", "json", "socks"] }
# Shared URL-hash + staging helpers for native-only resolvers (git / http)
sha2 = { workspace = true, optional = true }
tempfile = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
tempfile = { workspace = true, optional = true }
# Git clone (gated on `native-git`)
gix = { workspace = true, features = [
gix = { workspace = true, features = [
"blocking-http-transport-reqwest-rust-tls",
"blocking-network-client",
], optional = true }
# HTTP tarball extraction (gated on `http-tarball`)
# Uses libdeflater to match pm's extractor and avoid pulling a second gzip
# implementation (flate2) into the workspace.
libdeflater = { version = "1.25", optional = true }
tar = { version = "0.4", optional = true }
libdeflater = { version = "1.25", optional = true }
tar = { version = "0.4", optional = true }

# Async runtime (works on both native and WASM with forked tokio)
[dependencies.tokio]
workspace = true

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt"] }
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }

# Native (non-macOS) targets: reqwest's default rustls + ring.
[target.'cfg(not(any(target_arch = "wasm32", target_os = "macos")))'.dependencies]
Expand Down
90 changes: 64 additions & 26 deletions crates/ruborist/src/resolver/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ use crate::model::graph::{DependencyGraph, FindResult, PackageNode};
use crate::model::manifest::NodeManifest;
use crate::model::node::EdgeType;
use crate::model::package_json::PackageJson;
#[cfg(not(target_arch = "wasm32"))]
use crate::resolver::preload::{PreloadConfig, preload_manifests};
use crate::resolver::registry::{ResolveError, resolve_registry_dep};
use crate::spec::{Catalogs, PackageSpec, Protocol};
use crate::traits::progress::{BuildEvent, EventReceiver, NoopReceiver};
use crate::traits::registry::{RegistryClient, ResolvedPackage};
use crate::traits::registry::{MaybeSend, MaybeSync, RegistryClient, ResolvedPackage};

/// Dispatch a git/github spec to the real `gix`-backed resolver when the
/// `native-git` feature is enabled, otherwise error with a hint.
Expand Down Expand Up @@ -119,11 +120,19 @@ pub struct BuildDepsConfig {
pub catalogs: Catalogs,
}

/// Fallback concurrency for wasm where the worker-pool preload module is
/// gated out. wasm runs serial BFS resolution so this only sizes the inner
/// `FuturesUnordered` for transitive fanout — keep it modest.
#[cfg(target_arch = "wasm32")]
const DEFAULT_CONCURRENCY: usize = 16;
#[cfg(not(target_arch = "wasm32"))]
const DEFAULT_CONCURRENCY: usize = crate::resolver::preload::DEFAULT_CONCURRENCY;

impl Default for BuildDepsConfig {
fn default() -> Self {
Self {
peer_deps: PeerDeps::Skip,
concurrency: crate::resolver::preload::DEFAULT_CONCURRENCY,
concurrency: DEFAULT_CONCURRENCY,
skip_preload: false,
cache_dir: dirs::home_dir().map(|d| d.join(".cache/nm")),
git_clone_cache: Arc::new(GitCloneCache::new()),
Expand Down Expand Up @@ -450,7 +459,7 @@ async fn process_file_dep<E>(
///
/// # Returns
/// The result of processing (reused, created, or skipped)
pub async fn process_dependency<R: RegistryClient>(
pub async fn process_dependency<R: RegistryClient + MaybeSync>(
graph: &mut DependencyGraph,
registry: &R,
node_index: NodeIndex,
Expand Down Expand Up @@ -667,11 +676,14 @@ pub async fn process_dependency<R: RegistryClient>(
/// // Add initial dependency edges to root...
/// build_deps(&mut graph, &registry, PeerDeps::Include).await?;
/// ```
pub async fn build_deps<R: RegistryClient>(
pub async fn build_deps<R: RegistryClient + Clone + MaybeSend + MaybeSync + 'static>(
graph: &mut DependencyGraph,
registry: &R,
peer_deps: PeerDeps,
) -> Result<(), ResolveError<R::Error>> {
) -> Result<(), ResolveError<R::Error>>
where
R::Error: MaybeSend,
{
let config = BuildDepsConfig::default().with_peer_deps(peer_deps);
build_deps_with_config(graph, registry, config, &NoopReceiver).await
}
Expand All @@ -689,12 +701,18 @@ pub async fn build_deps<R: RegistryClient>(
/// * `registry` - Registry client for fetching packages
/// * `peer_deps` - How to handle peer dependencies
/// * `receiver` - Event receiver for handling build events
pub async fn build_deps_with_receiver<R: RegistryClient, E: EventReceiver>(
pub async fn build_deps_with_receiver<
R: RegistryClient + Clone + MaybeSend + MaybeSync + 'static,
E: EventReceiver,
>(
graph: &mut DependencyGraph,
registry: &R,
peer_deps: PeerDeps,
receiver: &E,
) -> Result<(), ResolveError<R::Error>> {
) -> Result<(), ResolveError<R::Error>>
where
R::Error: MaybeSend,
{
let config = BuildDepsConfig::default().with_peer_deps(peer_deps);
build_deps_with_config(graph, registry, config, receiver).await
}
Expand All @@ -719,20 +737,28 @@ pub async fn build_deps_with_receiver<R: RegistryClient, E: EventReceiver>(
///
/// build_deps_with_config(&mut graph, &registry, config, &receiver).await?;
/// ```
pub async fn build_deps_with_config<R: RegistryClient, E: EventReceiver>(
pub async fn build_deps_with_config<
R: RegistryClient + Clone + MaybeSend + MaybeSync + 'static,
E: EventReceiver,
>(
graph: &mut DependencyGraph,
registry: &R,
config: BuildDepsConfig,
receiver: &E,
) -> Result<(), ResolveError<R::Error>> {
) -> Result<(), ResolveError<R::Error>>
where
R::Error: MaybeSend,
{
tracing::debug!(
"Starting dependency tree build, peer_deps: {:?}, concurrency: {}, skip_preload: {}",
config.peer_deps,
config.concurrency,
config.skip_preload
);

// Phase 1: Preload manifests in parallel (unless skipped)
// Phase 1: Preload manifests in parallel (unless skipped).
// wasm has no worker-pool — preload runs serially in BFS instead.
#[cfg(not(target_arch = "wasm32"))]
run_preload_phase(graph, registry, &config, receiver).await;

// Phase 2: BFS traversal to build the dependency tree
Expand All @@ -746,12 +772,18 @@ pub async fn build_deps_with_config<R: RegistryClient, E: EventReceiver>(
}

/// Run the preload phase to warm up the cache with manifests.
async fn run_preload_phase<R: RegistryClient, E: EventReceiver>(
#[cfg(not(target_arch = "wasm32"))]
async fn run_preload_phase<
R: RegistryClient + Clone + MaybeSend + MaybeSync + 'static,
E: EventReceiver,
>(
graph: &DependencyGraph,
registry: &R,
config: &BuildDepsConfig,
receiver: &E,
) {
) where
R::Error: MaybeSend,
{
if config.skip_preload {
return;
}
Expand All @@ -764,18 +796,20 @@ async fn run_preload_phase<R: RegistryClient, E: EventReceiver>(
}

tracing::debug!("Preload phase: {} initial dependencies", initial_deps.len());
receiver.on_event(BuildEvent::PreloadStart {
count: initial_deps.len(),
});

let preload_config = PreloadConfig {
peer_deps: config.peer_deps,
concurrency: config.concurrency,
};

// Wrap registry in Arc for the worker pool. Each worker holds an
// Arc<R> so resolution runs on tokio's global executor independently
// of the main task driving result aggregation.
let registry_arc = std::sync::Arc::new(registry.clone());

let stats = preload_manifests(
initial_deps,
registry,
registry_arc,
preload_config,
receiver,
|_name, _manifest| {
Expand All @@ -784,11 +818,6 @@ async fn run_preload_phase<R: RegistryClient, E: EventReceiver>(
)
.await;

tracing::debug!(
"Preload phase completed: {} success, {} failed",
stats.success_count,
stats.failed_count
);
receiver.on_event(BuildEvent::PreloadComplete {
success: stats.success_count,
failed: stats.failed_count,
Expand All @@ -798,7 +827,7 @@ async fn run_preload_phase<R: RegistryClient, E: EventReceiver>(
}

/// Run the BFS traversal phase to build the dependency tree.
async fn run_bfs_phase<R: RegistryClient, E: EventReceiver>(
async fn run_bfs_phase<R: RegistryClient + MaybeSync, E: EventReceiver>(
graph: &mut DependencyGraph,
registry: &R,
config: &BuildDepsConfig,
Expand Down Expand Up @@ -921,10 +950,13 @@ use std::path::Path;
/// let pkg: PackageJson = serde_json::from_str(&pkg_content)?;
/// let lock = resolve(&pkg, &registry).await?;
/// ```
pub async fn resolve<R: RegistryClient>(
pub async fn resolve<R: RegistryClient + Clone + MaybeSend + MaybeSync + 'static>(
pkg: &PackageJson,
registry: &R,
) -> Result<PackageLock, ResolveError<R::Error>> {
) -> Result<PackageLock, ResolveError<R::Error>>
where
R::Error: MaybeSend,
{
resolve_with_options(pkg, registry, PeerDeps::Include, &NoopReceiver).await
}

Expand All @@ -935,12 +967,18 @@ pub async fn resolve<R: RegistryClient>(
/// * `registry` - Registry client for fetching packages
/// * `peer_deps` - How to handle peer dependencies
/// * `receiver` - Event receiver for progress tracking
pub async fn resolve_with_options<R: RegistryClient, E: EventReceiver>(
pub async fn resolve_with_options<
R: RegistryClient + Clone + MaybeSend + MaybeSync + 'static,
E: EventReceiver,
>(
pkg: &PackageJson,
registry: &R,
peer_deps: PeerDeps,
receiver: &E,
) -> Result<PackageLock, ResolveError<R::Error>> {
) -> Result<PackageLock, ResolveError<R::Error>>
where
R::Error: MaybeSend,
{
// Create graph with root node
let mut graph = DependencyGraph::from_package_json(PathBuf::from("."), pkg.clone());

Expand Down
Loading
Loading