Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
14fa6f6
docs(architecture): design for incremental index maintenance (base+de…
githubrobbi Jun 26, 2026
4708a19
feat(idxdelta): phase-0 measurement rig — build stamp + per-step appl…
githubrobbi Jun 26, 2026
333a441
docs(idxdelta): record measured baseline + re-sequence phases by cost
githubrobbi Jun 26, 2026
e384995
feat(idxdelta): phase 1 — incremental compute_path_lengths
githubrobbi Jun 26, 2026
6ce5e16
docs(idxdelta): mark Phase 1 done in tracking table
githubrobbi Jun 26, 2026
976127b
feat(idxdelta): phase 2 scaffold — IndexDelta overlay type
githubrobbi Jun 26, 2026
c604c00
docs(idxdelta): mark IndexDelta type done; note field deferred to Pha…
githubrobbi Jun 26, 2026
388529d
fix(idxdelta): phase 1 — delete-only batch must not full-recompute paths
githubrobbi Jun 26, 2026
f20fa48
feat(idxdelta): rig auto-syncs ~/bin from the real build dir + assert…
githubrobbi Jun 26, 2026
e6f34d3
feat(idxdelta): phase 2a — trigram_search base+delta choke point (plu…
githubrobbi Jun 26, 2026
5ea3b26
refactor(compact): decompose compact.rs (1363 → 385 lines) into submo…
githubrobbi Jun 26, 2026
d9c5962
feat(idxdelta): phase 2b — apply populates trigram delta, no per-tick…
githubrobbi Jun 26, 2026
8ffe603
test(idxdelta): scale verify bursts to 100k + measure true apply latency
githubrobbi Jun 26, 2026
8a0f07e
perf(idxdelta): refold directly when a batch will cross the compactio…
githubrobbi Jun 26, 2026
5132488
fix(idxdelta): verify rig tolerates a locked broker exe + drop dead c…
githubrobbi Jun 27, 2026
6627ab2
fix(idxdelta): build-id guard only fails on build-affecting changes
githubrobbi Jun 27, 2026
7db8965
test(idxdelta): unambiguous rename/delete smoke on unique sentinels
githubrobbi Jun 27, 2026
5f555bf
perf(idxdelta): phase 3 — Arc-share the base CSR indexes (cheaper clone)
githubrobbi Jun 27, 2026
8043b49
feat(idxdelta): phase 4a — extension delta overlay (no per-apply ext …
githubrobbi Jun 27, 2026
1b4e843
feat(idxdelta): phase 4b — children delta overlay (no per-apply child…
githubrobbi Jun 27, 2026
271ed55
test(idxdelta): overlay read-path microbench (Phase 4 churn overhead)
githubrobbi Jun 27, 2026
4460282
feat(daemon): phase 5 — apply cadence is debounce + max-wait (snappy,…
githubrobbi Jun 27, 2026
cc631ed
refactor(idxdelta): graduate phase-6 dev instrumentation to a perf guard
githubrobbi Jun 27, 2026
d982737
docs: fix workspace intra-doc links and enforce the rustdoc gate
githubrobbi Jun 27, 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
2 changes: 1 addition & 1 deletion .github/workflows/pr-fast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ jobs:
shared-key: pr-fast-sanity
save-if: 'false'

- run: cargo doc --workspace --all-features --no-deps --locked
- run: cargo doc --workspace --all-features --no-deps --locked --document-private-items
- run: cargo test --doc --workspace --all-features --locked

# ─────────────────────────────────────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion crates/uffs-bench/src/host/mock.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2025-2026 SKY, LLC.

//! In-memory [`Host`] for deterministic, OS-independent unit tests.
//! In-memory [`super::Host`] for deterministic, OS-independent unit tests.
//!
//! [`MockHost`] keeps an in-memory filesystem, records every call in order (so
//! tests can assert *snapshot-before-mutate* ordering and that the command
Expand Down
4 changes: 2 additions & 2 deletions crates/uffs-bench/src/stages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ fn step_from_output(out: &ProcOutput, output_path: &Path, label: &str) -> StepRe
/// Stage 1 — cross-tool head-to-head (run the harness).
///
/// The daemon run-state restore (R1) is registered once, up front, in
/// [`crate::run`] — before the daemon is first killed — so it is not re-taken
/// [`crate::run()`] — before the daemon is first killed — so it is not re-taken
/// per stage here (by stage time the as-found state is already gone).
fn run_cross_tool(
host: &dyn Host,
Expand All @@ -558,7 +558,7 @@ fn run_cross_tool(

/// Stage 2 — per-drive parity (+R2 cache backup when purging, run the script).
///
/// R1 daemon run-state is restored once via [`crate::run`] (see
/// R1 daemon run-state is restored once via [`crate::run()`] (see
/// [`run_cross_tool`]); only the per-drive cache backup is stage-local.
fn run_parity(host: &dyn Host, guard: &mut RunGuard<'_>, cfg: &StageCfg) -> Result<StepResult> {
if cfg.drop_cache {
Expand Down
4 changes: 2 additions & 2 deletions crates/uffs-client/src/connect_platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
//! [`crate::connect::UffsClient`] (async variant).
//!
//! Extracted from `connect.rs` for file-size policy compliance.
//! All items live on [`UffsClient`] via split `impl` blocks — no
//! public surface moves. Mirrors the sync-path split in
//! All items live on [`crate::connect::UffsClient`] via split `impl` blocks —
//! no public surface moves. Mirrors the sync-path split in
//! `connect_sync_platform.rs`.

use tokio::io::BufReader;
Expand Down
12 changes: 7 additions & 5 deletions crates/uffs-client/src/connect_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,11 @@ impl UffsClientSync {
///
/// # Deadline
///
/// On Windows, arms the [`crate::windows_deadline::WindowsDeadlineGuard`]
/// before any I/O and disarms it on return (success or error).
/// Using a [`DisarmOnDrop`] guard makes the disarm robust against
/// early-return paths, including `?` bubbling from the read loop.
/// On Windows, arms the `WindowsDeadlineGuard` (in
/// `crate::windows_deadline`) before any I/O and disarms it on return
/// (success or error). Using a `DisarmOnDrop` guard makes the disarm
/// robust against early-return paths, including `?` bubbling from the
/// read loop.
///
/// On Unix, the deadline is enforced by `SO_RCVTIMEO` /
/// `SO_SNDTIMEO` set at connect time and needs no per-call logic.
Expand Down Expand Up @@ -649,7 +650,8 @@ impl UffsClientSync {
/// single `status` call, saving one full RPC round-trip per CLI
/// invocation (~5–10 ms on Windows named pipes). Skippable via
/// `UFFS_CLIENT_SKIP_HEALTH_CHECK=1` (see
/// [`deep_health_check_enabled`]). Cost: ~200–600 µs local IPC.
/// [`crate::daemon_ctl::deep_health_check_enabled`]). Cost: ~200–600 µs
/// local IPC.
///
/// # Errors
///
Expand Down
7 changes: 4 additions & 3 deletions crates/uffs-client/src/daemon_child.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ impl DaemonChildHandle {
}
}

/// Returns the spawned daemon's PID, or `0` for [`Self::opaque`].
/// Returns the spawned daemon's PID, or `0` for `Self::opaque`
/// (Windows-only).
#[must_use]
pub(crate) const fn pid(&self) -> u32 {
self.pid
Expand All @@ -126,8 +127,8 @@ impl DaemonChildHandle {
/// surface as `101`, clap parse errors as `2`, graceful exit as `0`).
/// * `Err(err)` — the poll itself failed (treat as unknown, keep retrying).
///
/// For [`Self::opaque`] handles this is a no-op and always returns
/// `Ok(None)`.
/// For `Self::opaque` (Windows-only) handles this is a no-op and always
/// returns `Ok(None)`.
///
/// # Errors
///
Expand Down
8 changes: 8 additions & 0 deletions crates/uffs-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ harness = false
name = "search_benchmarks"
harness = false

[[bench]]
name = "overlay_read"
harness = false

[[bench]]
name = "apply_cost"
harness = false

# ─────────────────────────────────────────────────────────────────────────────
# Lints (inherit from workspace)
# ─────────────────────────────────────────────────────────────────────────────
Expand Down
247 changes: 247 additions & 0 deletions crates/uffs-core/benches/apply_cost.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2025-2026 SKY, LLC.

//! Per-apply USN-patch cost — the incremental-index-maintenance perf guard.
//!
//! [`apply_usn_patch`] is the hot path the USN journal loop runs on every poll:
//! it mutates the record columns in O(changed), overlays the batch onto the
//! base ∪ delta trigram / extension / children indexes, and refreshes the
//! touched records' `path_len`. The whole point of the project is that this
//! cost scales with the **batch size**, not the **drive size** — a 256-change
//! poll on a 4-million-record drive must not re-pay an O(total) rebuild.
//!
//! This bench locks that in. The fixture is a ~500k-record drive; each subject
//! applies a representative batch to a **fresh clone** (clone excluded from the
//! timing via `iter_batched`), so what is measured is the apply alone. The
//! profiles span the realistic USN-poll shapes:
//! * `creates/256` — a typical settle-debounced poll batch
//! * `creates/4000` — a heavy burst (bulk extract / installer)
//! * `mixed/4000` — creates + deletes + file renames interleaved
//! * `deletes/4000` — the tombstone path
//!
//! It is a guard, not a target: a regression shows up as a profile's time
//! jumping with the **fixture** size (it should not — only
//! `compute_path_lengths` in the >50k fallback is O(total), and these batches
//! stay under that), or a batch's per-change cost ballooning. The numbers
//! replace the ad-hoc development timing the project carried while the overlay
//! was being built.
//!
//! Run with: `cargo bench --bench apply_cost`
//!
//! Reference baseline (Apple M-series, ~500k-record fixture):
//!
//! ```text
//! profile time/batch
//! creates/256 ~120 µs
//! creates/4000 ~1.9 ms
//! mixed/4000 ~2.4 ms
//! deletes/4000 ~1.1 ms
//! ```

// The bench binary links uffs-core's full dependency set but uses only a
// subset; this is a structural fact of compiling a bench inside the crate, not
// a code-quality lint to fix.
#![expect(
unused_crate_dependencies,
reason = "bench links uffs-core's full dependency set but uses only a subset"
)]

use core::hint::black_box;

use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use uffs_core::compact::{
ChildrenIndex, CompactRecord, DriveCompactIndex, ExtensionIndex, IndexSource, apply_usn_patch,
};
use uffs_core::compact_storage::ColumnStorage;
use uffs_core::trigram::TrigramIndex;
use uffs_mft::usn::FileChange;
use uffs_text::case_fold::CaseFold;

/// Directories created directly under the fixture root.
const NUM_DIRS: usize = 2_000;
/// Files created in each directory; `NUM_DIRS * FILES_PER_DIR` ≈ 500k records,
/// a realistic multi-hundred-thousand-record drive.
const FILES_PER_DIR: usize = 250;
/// Extensions cycled across the fixture files, interned at ids 1..=5 (id 0 = no
/// extension). The leading const guarantees `EXTS` is never empty, so every
/// `index % EXTS.len()` below is a valid offset.
const EXTS: [&str; 5] = ["txt", "rs", "log", "json", "bin"];

/// The fixture extension for the `index`-th file, wrapping across [`EXTS`].
/// Total (panic-free): `index % EXTS.len()` is always a valid offset, and the
/// const-asserted non-empty `EXTS` makes the `unwrap_or` fallback unreachable.
fn ext_at(index: usize) -> &'static str {
EXTS.get(index % EXTS.len()).copied().unwrap_or("bin")
}

/// The 1-based extension id (interned offset into the drive's `ext_names`) for
/// the `index`-th file; 0 is reserved for "no extension".
fn ext_id_at(index: usize) -> u16 {
// index % EXTS.len() ∈ 0..5, so +1 ∈ 1..=5 — always fits u16.
u16::try_from(index % EXTS.len() + 1).unwrap_or(0)
}

/// Append one record + its name bytes to the growing fixture columns.
fn push_file(
names: &mut Vec<u8>,
records: &mut Vec<CompactRecord>,
name: &str,
parent: u32,
is_dir: bool,
ext_id: u16,
) {
let name_offset = u32::try_from(names.len()).unwrap_or(u32::MAX);
names.extend_from_slice(name.as_bytes());
records.push(CompactRecord {
name_offset,
flags: if is_dir { 0x10 } else { 0 },
parent_idx: parent,
name_len: u16::try_from(name.len()).unwrap_or(u16::MAX),
extension_id: ext_id,
name_first_byte: name.as_bytes().first().copied().unwrap_or(0),
..CompactRecord::default()
});
}

/// Build the base drive with `delta = None` (cold-load / post-compaction
/// state). The trigram base is left empty (apply overlays it via the delta
/// either way) to keep fixture setup fast; children + ext are real CSR builds.
fn build_drive() -> DriveCompactIndex {
let mut names: Vec<u8> = Vec::new();
let mut records: Vec<CompactRecord> = Vec::new();

push_file(&mut names, &mut records, "C", u32::MAX, true, 0);
for dir in 0..NUM_DIRS {
push_file(&mut names, &mut records, &format!("dir{dir}"), 0, true, 0);
}
for dir in 0..NUM_DIRS {
let dir_idx = u32::try_from(1 + dir).unwrap_or(u32::MAX);
for file in 0..FILES_PER_DIR {
let name = format!("file{dir}_{file}.{}", ext_at(file));
push_file(
&mut names,
&mut records,
&name,
dir_idx,
false,
ext_id_at(file),
);
}
}

let fold = CaseFold::default_table();
let children = ChildrenIndex::build(&records);
let ext_index = ExtensionIndex::build(&records);
let frs_to_compact: Vec<u32> = (0..records.len())
.map(|idx| u32::try_from(idx).unwrap_or(u32::MAX))
.collect();
let ext_names: Vec<Box<str>> = core::iter::once(Box::from(""))
.chain(EXTS.iter().map(|ext| Box::from(*ext)))
.collect();
DriveCompactIndex {
letter: uffs_mft::platform::DriveLetter::T,
records: ColumnStorage::from_vec(records),
names: ColumnStorage::from_vec(names),
trigram: TrigramIndex::empty().into(),
children: children.into(),
ext_index: ext_index.into(),
fold,
ext_names,
source: IndexSource::MftFile(std::path::PathBuf::from("T:")),
source_epoch: 1,
bloom: None,
path_trie: None,
frs_to_compact,
delta: None,
}
}

/// FRS of an existing base file record (directory `dir`, file `file`); frs ==
/// idx in the base, so this also serves as a valid `parent_frs` for a create.
const fn file_frs(dir: usize, file: usize) -> u64 {
(1 + NUM_DIRS + dir * FILES_PER_DIR + file) as u64
}

/// `count` pure creates spread across the directories (new FRNs past the base).
fn creates(base_count: usize, count: usize) -> Vec<FileChange> {
(0..count)
.map(|idx| FileChange {
frs: ((base_count + idx) as u64).into(),
parent_frs: ((idx % NUM_DIRS) as u64 + 1).into(),
filename: format!("new{idx}.{}", ext_at(idx)),
created: true,
..FileChange::default()
})
.collect()
}

/// `count` deletes of existing base file records (the tombstone path).
fn deletes(count: usize) -> Vec<FileChange> {
(0..count)
.map(|idx| FileChange {
frs: file_frs(idx % NUM_DIRS, idx % FILES_PER_DIR).into(),
deleted: true,
..FileChange::default()
})
.collect()
}

/// `count` interleaved create / delete / file-rename changes — the realistic
/// installer/extract shape that exercises every apply branch in one batch.
fn mixed(base_count: usize, count: usize) -> Vec<FileChange> {
(0..count)
.map(|idx| {
let dir = idx % NUM_DIRS;
let file = idx % FILES_PER_DIR;
match idx % 3 {
0 => FileChange {
frs: ((base_count + idx) as u64).into(),
parent_frs: (dir as u64 + 1).into(),
filename: format!("add{idx}.{}", ext_at(idx)),
created: true,
..FileChange::default()
},
1 => FileChange {
frs: file_frs(dir, file).into(),
deleted: true,
..FileChange::default()
},
_ => FileChange {
frs: file_frs(dir, file).into(),
parent_frs: (dir as u64 + 1).into(),
filename: format!("renamed{idx}.{}", ext_at(idx)),
renamed: true,
..FileChange::default()
},
}
})
.collect()
}

/// Time `apply_usn_patch` for each batch profile against a fresh fixture clone.
fn bench_apply(crit: &mut Criterion) {
let base = build_drive();
let base_count = base.records.len();

let profiles: [(&str, Vec<FileChange>); 4] = [
("creates/256", creates(base_count, 256)),
("creates/4000", creates(base_count, 4_000)),
("mixed/4000", mixed(base_count, 4_000)),
("deletes/4000", deletes(4_000)),
];

let mut group = crit.benchmark_group("apply_cost");
for (name, batch) in &profiles {
group.bench_function(*name, |bencher| {
bencher.iter_batched(
|| base.clone(),
|mut drive| black_box(apply_usn_patch(&mut drive, black_box(batch))),
BatchSize::SmallInput,
);
});
}
group.finish();
}

criterion_group!(benches, bench_apply);
criterion_main!(benches);
Loading
Loading