Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3a48385
perf(pm): preload multi-thread worker pool (tokio::spawn)
elrrrrrrr Apr 27, 2026
043b35b
perf(ruborist): normalize_spec returns Cow, no alloc on common path
elrrrrrrr Apr 25, 2026
3df0402
perf(pm): default file log filter to info+ instead of debug+
elrrrrrrr Apr 25, 2026
7e4c3c8
perf(ruborist): parallelise workspace package.json reads
elrrrrrrr Apr 25, 2026
6ab49b7
perf(pm): chunked parallel writes within tarball extraction
elrrrrrrr Apr 21, 2026
40e554a
perf(pm): bump manifests-concurrency cap 64 → 128
elrrrrrrr Apr 30, 2026
7243a5b
ci: retrigger bench for PR #2872 second sample (verify wall vs PR #28…
elrrrrrrr Apr 30, 2026
b79a8f8
ci: retrigger bench for PR #2872 sample 3 (need clean p1+p3 in same run)
elrrrrrrr Apr 30, 2026
0ba3639
ci: sample 4 — chase clean (fast-bun + clean-utoo) slot
elrrrrrrr Apr 30, 2026
610ebb6
perf(pm): manifests-concurrency cap 128 → 96
elrrrrrrr Apr 30, 2026
c7ce742
ci: sample 6 — retry cap=96 (s5 was σ=1.08 noisy)
elrrrrrrr Apr 30, 2026
4300e37
ci: sample 7 — cap=96 confirmation pass (s6 ratio 1.55, need 2nd clean)
elrrrrrrr Apr 30, 2026
ca6e253
perf(ruborist): rotate DNS addresses per-family (cherry-pick from PR …
elrrrrrrr Apr 30, 2026
a28045b
perf(ruborist): force HTTP/1.1 with idle pool 256 (drop H2 head-of-li…
elrrrrrrr Apr 30, 2026
249b48a
ci: sample 10 — verify s9 ratio 1.03 (HTTP/1.1 stack) reproducibility
elrrrrrrr Apr 30, 2026
0b792f9
perf(pm): cap blocking pool + raise manifests-concurrency 96 → 128
elrrrrrrr Apr 30, 2026
cd83807
ci: sample 12 — confirm s11 (cap=128+maxBT) p1=2.50s reproducibility
elrrrrrrr Apr 30, 2026
4a2db67
perf(ruborist): drop redundant pool_max_idle_per_host (cap covers it)
elrrrrrrr Apr 30, 2026
cafb7f9
ci: sample 14 — retry drop-pool stack (s13 p1 σ=1.54 unusable)
elrrrrrrr Apr 30, 2026
5c877ed
ci: sample 15 — verify if drop-pool causes p0/p1 crash (s14 had 0.00s)
elrrrrrrr May 1, 2026
1823e28
ci: sample 16 — final convergence validation on cap=128+maxBT+drop-po…
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/pm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,14 @@ 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; ~worker_threads count is enough to
// saturate a 4-10 core machine on short CPU-bound tasks while
// keeping the pool warm. Default pool gave bimodal walls
// (2.7s fast / 6.9s thrash on M2); capping eliminates thrash.
.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
19 changes: 17 additions & 2 deletions crates/pm/src/util/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,23 @@ pub fn init_tracing(verbose: bool) -> Result<(PathBuf, WorkerGuard)> {
let console_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(format!("utoo={console_level}")));

// File filter: always capture debug+ for troubleshooting
let file_filter = EnvFilter::new("utoo=debug");
// File filter: capture info+ by default. Hot-path debug!() calls
// (cache hits, per-package preload events, BFS dispatch) emit
// ~5-10 events per resolved manifest. With 2730+ manifests during
// a cold preload that's 15-30k events that — even routed through
// the non_blocking appender's channel — pay format/serialise CPU
// on the resolving thread. CI standalone manifest-bench reaches
// avg_conc=92 at cap=128 with the same reqwest stack; ruborist
// sat at avg_conc=56 after every other Mutex/clone hot-path was
// eliminated. Tracing was the last remaining shared cost.
//
// Override via `UTOO_FILE_LOG=debug` (or any RUST_LOG-style spec)
// to bring the troubleshooting captures back when actually
// diagnosing.
let file_filter = match env::var("UTOO_FILE_LOG") {
Ok(spec) if !spec.is_empty() => EnvFilter::new(spec),
_ => EnvFilter::new("utoo=info"),
};

// 2. Create log file
let timestamp = SystemTime::now()
Expand Down
18 changes: 16 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,23 @@ pub fn get_install_scope() -> InstallScope {
INSTALL_SCOPE.get().copied().unwrap_or_default()
}

// Manifest fetch concurrency configuration
// Manifest fetch concurrency configuration.
//
// Sweep history under worker-pool + HTTP/1.1 + DNS per-family rotation:
// cap=64 wall=3.10s avg_conc=56 (FuturesUnordered ceiling)
// cap=96 wall=2.87-3.08s ratio 0.83-1.03 (s9-s10 with H1 stack)
// cap=128 wall=2.15s avg_conc=84 (PR #2818 historical sweet spot, inline parse)
// cap=160 wall=2.14s avg_conc=119 (Cloudflare per-IP throttle bites)
//
// 96 was chosen empirically when burst aggression on H2 was triggering
// Cloudflare throttle (pre-H1 era). Once `.http1_only()` landed and
// requests stopped multiplexing through a single TCP connection, cap=128
// becomes the right setting again — independent TCP streams distribute
// across the rotated DNS pool without tripping per-IP throttle, and the
// extra 33% concurrency saturates more of the Cloudflare connection
// budget.
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
1 change: 1 addition & 0 deletions crates/ruborist/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
//! }).await?;
//! ```

pub mod maybe_send;
pub mod model;
pub mod resolver;
pub mod service;
Expand Down
27 changes: 27 additions & 0 deletions crates/ruborist/src/maybe_send.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! `Send`/`Sync` shim that is `Send`/`Sync` on native targets and
//! no-op on `wasm32`. Lets the resolver share a single trait surface
//! across multi-thread tokio (where futures must be `Send` to be
//! `tokio::spawn`-able) and wasm-bindgen (where `JsFuture` is `!Send`
//! and tasks run via `wasm_bindgen_futures::spawn_local`).

/// `Send` on native, no-op on wasm32.
#[cfg(not(target_arch = "wasm32"))]
pub trait MaybeSend: Send {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send> MaybeSend for T {}

#[cfg(target_arch = "wasm32")]
pub trait MaybeSend {}
#[cfg(target_arch = "wasm32")]
impl<T> MaybeSend for T {}

/// `Sync` on native, no-op on wasm32.
#[cfg(not(target_arch = "wasm32"))]
pub trait MaybeSync: Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Sync> MaybeSync for T {}

#[cfg(target_arch = "wasm32")]
pub trait MaybeSync {}
#[cfg(target_arch = "wasm32")]
impl<T> MaybeSync for T {}
Loading
Loading