From dd9855bbaa1bd8ab904ca36fc7677ca43e047e64 Mon Sep 17 00:00:00 2001 From: Harold Hunt Date: Wed, 14 Oct 2026 22:27:44 -0400 Subject: [PATCH] Fix quadratic marking, path resolution, and removal review Batch marked-path resolution and cache display crumbs across unchanged frames. Bound removal nesting, index queue mutations, absorb descendants in one pass, and index Review status from each fresh plan. Include deterministic state/window work budgets, path and invalidation regression coverage, and measured red/green evidence. Removal authorization remains freshly revalidated before deletion. --- crates/disktree-app/src/marks.rs | 601 ++++++++++++++++-- crates/disktree-app/src/state.rs | 55 +- crates/disktree-app/src/tests.rs | 538 +++++++++++++++- crates/disktree-app/src/views.rs | 136 +++- crates/disktree-core/src/remote.rs | 34 +- crates/disktree-core/src/removal.rs | 135 +++- docs/performance/mark-resolution/README.md | 227 +++++++ .../mark-resolution/additional-audit.log | 50 ++ .../mark-resolution/additional-audit.md | 195 ++++++ .../mark-resolution/additional-audit.patch | 561 ++++++++++++++++ .../mark-resolution/expanded/README.md | 229 +++++++ .../expanded/green-marking.log | 54 ++ .../mark-resolution/expanded/green-review.log | 29 + .../mark-resolution/expanded/green.log | 57 ++ .../expanded/interactions-before.log | 65 ++ .../expanded/interactions-final.log | 66 ++ .../expanded/interactions-main.log | 66 ++ .../mark-resolution/expanded/lint.log | 7 + .../mark-resolution/expanded/red-folders.log | 23 + .../mark-resolution/expanded/red.log | 61 ++ .../mark-resolution/expanded/tests.log | 275 ++++++++ .../mark-resolution/expanded/wall-after.log | 127 ++++ .../mark-resolution/expanded/wall-before.log | 187 ++++++ docs/performance/mark-resolution/green.log | 40 ++ .../mark-resolution/interactions-green.log | 65 ++ .../mark-resolution/interactions-red.log | 66 ++ .../mark-resolution/interactions.md | 96 +++ docs/performance/mark-resolution/lint.log | 6 + docs/performance/mark-resolution/red-core.log | 33 + .../mark-resolution/red-dimensions.log | 17 + docs/performance/mark-resolution/red.log | 32 + docs/performance/mark-resolution/tests.log | 265 ++++++++ .../mark-resolution/wall-green-repeats.log | 126 ++++ .../mark-resolution/wall-red-repeats.log | 174 +++++ 34 files changed, 4562 insertions(+), 136 deletions(-) create mode 100644 docs/performance/mark-resolution/README.md create mode 100644 docs/performance/mark-resolution/additional-audit.log create mode 100644 docs/performance/mark-resolution/additional-audit.md create mode 100644 docs/performance/mark-resolution/additional-audit.patch create mode 100644 docs/performance/mark-resolution/expanded/README.md create mode 100644 docs/performance/mark-resolution/expanded/green-marking.log create mode 100644 docs/performance/mark-resolution/expanded/green-review.log create mode 100644 docs/performance/mark-resolution/expanded/green.log create mode 100644 docs/performance/mark-resolution/expanded/interactions-before.log create mode 100644 docs/performance/mark-resolution/expanded/interactions-final.log create mode 100644 docs/performance/mark-resolution/expanded/interactions-main.log create mode 100644 docs/performance/mark-resolution/expanded/lint.log create mode 100644 docs/performance/mark-resolution/expanded/red-folders.log create mode 100644 docs/performance/mark-resolution/expanded/red.log create mode 100644 docs/performance/mark-resolution/expanded/tests.log create mode 100644 docs/performance/mark-resolution/expanded/wall-after.log create mode 100644 docs/performance/mark-resolution/expanded/wall-before.log create mode 100644 docs/performance/mark-resolution/green.log create mode 100644 docs/performance/mark-resolution/interactions-green.log create mode 100644 docs/performance/mark-resolution/interactions-red.log create mode 100644 docs/performance/mark-resolution/interactions.md create mode 100644 docs/performance/mark-resolution/lint.log create mode 100644 docs/performance/mark-resolution/red-core.log create mode 100644 docs/performance/mark-resolution/red-dimensions.log create mode 100644 docs/performance/mark-resolution/red.log create mode 100644 docs/performance/mark-resolution/tests.log create mode 100644 docs/performance/mark-resolution/wall-green-repeats.log create mode 100644 docs/performance/mark-resolution/wall-red-repeats.log diff --git a/crates/disktree-app/src/marks.rs b/crates/disktree-app/src/marks.rs index 7a03e476..64a68724 100644 --- a/crates/disktree-app/src/marks.rs +++ b/crates/disktree-app/src/marks.rs @@ -4,17 +4,73 @@ //! re-scanned after a removal, and a mark must survive that (or be reported as //! gone) rather than silently pointing at a different node. +use std::collections::BTreeSet; +use std::ops::Bound::{Excluded, Unbounded}; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Weak}; use disktree_core::removal::Target; use disktree_core::tree::{Metric, Node}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; + +#[cfg(test)] +pub mod work { + use std::cell::Cell; + + thread_local! { + pub static RESOLUTION: Cell = const { Cell::new(0) }; + pub static FRAMES: Cell = const { Cell::new(0) }; + pub static RENDERS: Cell = const { Cell::new(0) }; + pub static PLANS: Cell = const { Cell::new(0) }; + pub static REFRESHES: Cell = const { Cell::new(0) }; + pub static TILES: Cell = const { Cell::new(0) }; + pub static MARKING: Cell = const { Cell::new(0) }; + pub static COMPACTION: Cell = const { Cell::new(0) }; + pub static REVIEW: Cell = const { Cell::new(0) }; + pub static REVIEW_ROWS: Cell = const { Cell::new(0) }; + pub static COVERED_ROWS: Cell = const { Cell::new(0) }; + pub static BLOCKED_ROWS: Cell = const { Cell::new(0) }; + } + + pub fn step() { + RESOLUTION.set(RESOLUTION.get() + 1); + } + + pub fn reset() { + RESOLUTION.set(0); + FRAMES.set(0); + RENDERS.set(0); + PLANS.set(0); + REFRESHES.set(0); + TILES.set(0); + MARKING.set(0); + COMPACTION.set(0); + REVIEW.set(0); + REVIEW_ROWS.set(0); + COVERED_ROWS.set(0); + BLOCKED_ROWS.set(0); + } +} /// Marked paths, in the order they were marked. #[derive(Debug, Default)] pub struct Marks { items: Vec, - index: FxHashSet, + // Path's component ordering makes descendants contiguous. The same + // index answers exact membership and whether a parent has any marks + // inside, without storing ancestor counts or scanning every mark. + index: BTreeSet, + resolved: Option, +} + +/// Retain only the allocation identity: a rescan must not keep the old tree +/// alive while a new one is being displayed. +#[derive(Debug)] +struct ResolvedMarks { + tree: Weak, + root_path: PathBuf, + metric: Metric, + crumbs: Arc>>, } impl Marks { @@ -22,6 +78,54 @@ impl Marks { self.index.contains(path) } + /// Marked ancestor with the shortest stored spelling, excluding the + /// path itself: the outermost one for paths built by the scan. Preserve + /// that choice even for raw queues with redundant path separators. + pub fn ancestor(&self, path: &Path) -> Option<&Path> { + path.ancestors() + .skip(1) + .filter_map(|ancestor| { + #[cfg(test)] + work::MARKING.set(work::MARKING.get() + 1); + self.index.get(ancestor) + }) + .min_by_key(|ancestor| ancestor.as_os_str().len()) + .map(PathBuf::as_path) + } + + /// Drop strict descendants in one stable compaction, retaining the + /// parent's mark and unrelated marks in their existing order. + pub fn absorb(&mut self, path: &Path) -> usize { + #[cfg(test)] + work::MARKING.set(work::MARKING.get() + 1); + let has_descendant = self + .index + .range::((Excluded(path), Unbounded)) + .next() + .is_some_and(|next| next.starts_with(path)); + // Most insertions are disjoint; proving that from the index keeps + // queuing M paths from doing M full scans of the ordered item list. + if !has_descendant { + return 0; + } + self.resolved = None; + let before = self.items.len(); + let index = &mut self.index; + self.items.retain(|item| { + #[cfg(test)] + work::COMPACTION.set(work::COMPACTION.get() + 1); + if item.path != path && item.path.starts_with(path) { + #[cfg(test)] + work::MARKING.set(work::MARKING.get() + 1); + index.remove(&item.path); + false + } else { + true + } + }); + before - self.items.len() + } + pub fn items(&self) -> &[Target] { &self.items } @@ -38,6 +142,7 @@ impl Marks { /// /// Returns `true` when the path ended up marked. pub fn toggle(&mut self, target: Target) -> bool { + self.resolved = None; if self.index.remove(&target.path) { self.items.retain(|item| item.path != target.path); false @@ -50,22 +155,34 @@ impl Marks { pub fn remove(&mut self, path: &Path) { if self.index.remove(path) { - self.items.retain(|item| item.path != path); + self.resolved = None; + self.items.retain(|item| { + #[cfg(test)] + work::COMPACTION.set(work::COMPACTION.get() + 1); + item.path != path + }); } } pub fn clear(&mut self) { + self.resolved = None; self.items.clear(); self.index.clear(); } - /// Re-read sizes from a freshly scanned tree and drop marks whose path no - /// longer exists, so the tally never claims space that is already gone. + /// Re-read sizes from a freshly scanned tree. Missing paths stay marked + /// with zero bytes, so they cannot claim space that is already gone. pub fn refresh(&mut self, root_path: &Path, root: &Node, _metric: Metric) { + #[cfg(test)] + work::REFRESHES.set(work::REFRESHES.get() + 1); + self.resolved = None; + let mut resolver = Resolver::new(root_path, root); let mut resolved = Vec::with_capacity(self.items.len()); for item in &self.items { - match find(root_path, root, &item.path) { - Some(node) => resolved.push(Target { + #[cfg(test)] + work::step(); + match resolver.resolve(&item.path) { + Some((_, node)) => resolved.push(Target { bytes: node.bytes, is_dir: node.is_dir(), hidden: is_hidden(&item.path), @@ -79,25 +196,90 @@ impl Marks { } self.items = resolved; } + + /// Crumbs are display state, never removal authorization. Rebuild on every + /// mark mutation, refresh, tree replacement, root change or metric change. + pub fn crumbs( + &mut self, + root_path: &Path, + tree: &Arc, + metric: Metric, + ) -> Arc>> { + if let Some(resolved) = &self.resolved + && resolved.tree.as_ptr() == Arc::as_ptr(tree) + && resolved.root_path == root_path + && resolved.metric == metric + { + return Arc::clone(&resolved.crumbs); + } + let mut resolver = Resolver::new(root_path, tree); + let mut crumbs = FxHashSet::default(); + for item in &self.items { + #[cfg(test)] + work::step(); + if let Some((path, _)) = resolver.resolve(&item.path) { + crumbs.insert(path); + } + } + let crumbs = Arc::new(crumbs); + self.resolved = Some(ResolvedMarks { + tree: Arc::downgrade(tree), + root_path: root_path.to_path_buf(), + metric, + crumbs: Arc::clone(&crumbs), + }); + crumbs + } } -/// Walk the tree to the node at an absolute path. Path components are compared -/// one at a time, so a name containing a path separator cannot confuse it. -pub fn find<'a>( - root_path: &Path, +/// A batch of path lookups indexes each visited directory's children once. +/// Work is O(S + M × D) expected hash operations: S sibling entries across +/// visited directories, M paths and D components per path. Unvisited subtrees +/// cost nothing, and no index is stored on the millions of scan nodes. +#[derive(Debug)] +pub struct Resolver<'a> { + root_path: &'a Path, root: &'a Node, - path: &Path, -) -> Option<&'a Node> { - let relative = path.strip_prefix(root_path).ok()?; - let mut node = root; - for component in relative.components() { - let name = component.as_os_str().to_string_lossy(); - node = node - .children - .iter() - .find(|child| child.name.as_ref() == name)?; - } - Some(node) + indexes: FxHashMap<*const Node, FxHashMap<&'a str, usize>>, +} + +impl<'a> Resolver<'a> { + pub fn new(root_path: &'a Path, root: &'a Node) -> Self { + Self { + root_path, + root, + indexes: FxHashMap::default(), + } + } + + /// Compare whole components with the scan's existing case-sensitive, + /// lossy name semantics. The first equal child wins, as in a linear walk. + pub fn resolve(&mut self, path: &Path) -> Option<(Vec, &'a Node)> { + let relative = path.strip_prefix(self.root_path).ok()?; + let mut node = self.root; + let mut crumbs = Vec::new(); + for component in relative.components() { + #[cfg(test)] + work::step(); + let name = component.as_os_str().to_string_lossy(); + let children = self + .indexes + .entry(std::ptr::from_ref(node)) + .or_insert_with(|| { + let mut children = FxHashMap::default(); + for (index, child) in node.children.iter().enumerate() { + #[cfg(test)] + work::step(); + children.entry(child.name.as_ref()).or_insert(index); + } + children + }); + let index = *children.get(name.as_ref())?; + crumbs.push(index); + node = &node.children[index]; + } + Some((crumbs, node)) + } } /// A dotfile or dot-directory by name. @@ -149,6 +331,266 @@ mod tests { } } + #[test] + fn refresh_resolution_budget() { + let measure = |count| { + let root_path = std::env::temp_dir().join("marks-budget"); + let mut root = Node::directory("root"); + let mut marks = Marks::default(); + for index in 0..count { + let name = format!("folder-{index:05}"); + let mut dir = Node::directory(name.as_str()); + dir.children.push(file("payload", 1)); + root.children.push(dir); + marks.toggle(Target { + path: root_path.join(name).join("payload"), + bytes: 0, + is_dir: false, + hidden: false, + }); + } + work::reset(); + let start = std::time::Instant::now(); + for _ in 0..4 { + marks.refresh(&root_path, &root, Metric::Bytes); + } + let elapsed = start.elapsed(); + let operations = work::RESOLUTION.get(); + assert!(marks.items().iter().all(|item| item.bytes == 1)); + eprintln!( + "refresh M={count} N={} R=4 work={operations} wall={elapsed:?}", + count * 2 + ); + operations + }; + let small = measure(128); + let large = measure(1024); + assert!(large <= small * 16, "8x marks/folders: {small} -> {large}"); + assert!(large <= 1024 * 4 * 16, "absolute work cap: {large}"); + } + + #[test] + fn cached_crumbs_follow_every_invalidation_boundary() { + let root_path = Path::new("/home/tobi"); + let mut root = Arc::new(tree()); + let mut marks = Marks::default(); + marks.toggle(target("/home/tobi/.cache", 0)); + let first = marks.crumbs(root_path, &root, Metric::Bytes); + assert!(first.contains(&vec![0])); + work::reset(); + assert!(Arc::ptr_eq( + &first, + &marks.crumbs(root_path, &root, Metric::Bytes) + )); + assert_eq!( + work::RESOLUTION.get(), + 0, + "unchanged frames need no lookup" + ); + assert_eq!( + Arc::strong_count(&root), + 1, + "the cache must not retain a scan" + ); + + marks.toggle(target("/home/tobi/notes.bin", 0)); + assert_eq!(marks.crumbs(root_path, &root, Metric::Bytes).len(), 2); + marks.remove(Path::new("/home/tobi/.cache")); + assert!( + marks + .crumbs(root_path, &root, Metric::Bytes) + .contains(&vec![1]) + ); + marks.clear(); + assert!(marks.crumbs(root_path, &root, Metric::Bytes).is_empty()); + marks.toggle(target("/home/tobi/notes.bin", 0)); + marks.toggle(target("/home/tobi/notes.bin", 0)); + assert!(marks.crumbs(root_path, &root, Metric::Bytes).is_empty()); + marks.toggle(target("/home/tobi/notes.bin", 0)); + + let before = marks.crumbs(root_path, &root, Metric::Bytes); + let files = marks.crumbs(root_path, &root, Metric::Files); + assert!(!Arc::ptr_eq(&before, &files), "metric changes invalidate"); + marks.refresh(root_path, &root, Metric::Files); + let refreshed = marks.crumbs(root_path, &root, Metric::Files); + assert!(!Arc::ptr_eq(&files, &refreshed), "refresh invalidates"); + assert!( + marks + .crumbs(Path::new("/elsewhere"), &root, Metric::Files) + .is_empty() + ); + assert!( + marks + .crumbs(root_path, &root, Metric::Files) + .contains(&vec![1]) + ); + + let old_tree = Arc::downgrade(&root); + let mut replacement = (*root).clone(); + replacement.children.reverse(); + root = Arc::new(replacement); + assert!(old_tree.upgrade().is_none(), "old scan is released"); + assert!( + marks + .crumbs(root_path, &root, Metric::Files) + .contains(&vec![0]) + ); + Arc::make_mut(&mut root).children.clear(); + assert!(marks.crumbs(root_path, &root, Metric::Files).is_empty()); + } + + #[test] + fn resolver_preserves_scan_name_semantics() { + let root_path = Path::new("/home/tobi"); + let mut root = tree(); + root.children.push(file("notes.bin", 200)); + let mut resolver = Resolver::new(root_path, &root); + let (crumbs, node) = resolver + .resolve(&root_path.join("notes.bin")) + .expect("file"); + assert_eq!(crumbs, [1]); + assert_eq!(node.bytes, 100, "first equal scan name wins"); + assert!(resolver.resolve(&root_path.join("NOTES.BIN")).is_none()); + assert!(resolver.resolve(&root_path.join("notes")).is_none()); + assert!( + resolver + .resolve(&root_path.join(".cache/missing")) + .is_none() + ); + assert!(resolver.resolve(root_path).expect("root").0.is_empty()); + } + + #[cfg(unix)] + #[test] + fn resolver_preserves_lossy_non_utf8_components() { + use std::os::unix::ffi::OsStrExt as _; + let root_path = Path::new("/synthetic"); + let name = std::ffi::OsStr::from_bytes(b"file-\xff"); + let mut root = Node::directory("root"); + root.children.push(file(&name.to_string_lossy(), 1)); + let mut resolver = Resolver::new(root_path, &root); + assert_eq!( + resolver + .resolve(&root_path.join(name)) + .expect("lossy scan name") + .0, + [0] + ); + } + + #[test] + fn ordered_marks_preserve_components_case_and_outermost_ancestor() { + let mut marks = Marks::default(); + for path in [ + "/synthetic/a/child", + "/synthetic/a", + "/synthetic/a-b", + "/synthetic/a.b", + "/synthetic/ab", + "/synthetic/A", + ] { + marks.toggle(target(path, 1)); + } + assert_eq!( + marks.ancestor(Path::new("/synthetic/a/child/file")), + Some(Path::new("/synthetic/a")) + ); + assert_eq!(marks.ancestor(Path::new("/synthetic/a")), None); + assert_eq!( + marks.ancestor(Path::new("/synthetic/a-b/file")), + Some(Path::new("/synthetic/a-b")) + ); + assert_eq!( + marks.ancestor(Path::new("/synthetic/A/file")), + Some(Path::new("/synthetic/A")) + ); + assert_eq!(marks.absorb(Path::new("/synthetic/a")), 1); + let expected: Vec<_> = [ + "/synthetic/a", + "/synthetic/a-b", + "/synthetic/a.b", + "/synthetic/ab", + "/synthetic/A", + ] + .map(PathBuf::from) + .into(); + assert_eq!( + marks + .items() + .iter() + .map(|item| item.path.clone()) + .collect::>(), + expected + ); + assert!(marks.items().iter().all(|item| marks.contains(&item.path))); + assert!(!marks.contains(Path::new("/synthetic/a/child"))); + marks.remove(Path::new("/synthetic/a")); + assert_eq!(marks.ancestor(Path::new("/synthetic/a/file")), None); + assert_eq!( + marks.absorb(Path::new("/synthetic/a")), + 0, + "prefix lookalikes stay marked" + ); + marks.clear(); + assert_eq!(marks.absorb(Path::new("/synthetic")), 0); + + // Raw control clients can supply redundant separators. Retain the + // original shortest-spelling rule rather than changing notices. + marks.toggle(target("/synthetic/a////////////////////", 1)); + marks.toggle(target("/synthetic/a/child", 1)); + assert_eq!( + marks.ancestor(Path::new("/synthetic/a/child/file")), + Some(Path::new("/synthetic/a/child")) + ); + } + + #[test] + fn absorption_invalidates_cached_crumbs_and_keeps_missing_paths_indexed() { + let root_path = Path::new("/home/tobi"); + let root = Arc::new(tree()); + let mut marks = Marks::default(); + marks.toggle(target("/home/tobi/.cache/blob.bin", 0)); + marks.toggle(target("/home/tobi/.cache", 0)); + marks.toggle(target("/home/tobi/gone/nested", 100)); + let before = marks.crumbs(root_path, &root, Metric::Bytes); + assert_eq!(before.len(), 2); + assert_eq!(marks.absorb(Path::new("/home/tobi/.cache")), 1); + let after = marks.crumbs(root_path, &root, Metric::Bytes); + assert!(!Arc::ptr_eq(&before, &after)); + assert_eq!(after.len(), 1); + assert!(after.contains(&vec![0])); + marks.refresh(root_path, &root, Metric::Files); + assert!(marks.contains(Path::new("/home/tobi/gone/nested"))); + assert_eq!(marks.absorb(Path::new("/home/tobi/gone")), 1); + assert!(!marks.contains(Path::new("/home/tobi/gone/nested"))); + } + + #[cfg(unix)] + #[test] + fn ordered_marks_keep_exact_non_utf8_paths() { + use std::os::unix::ffi::OsStrExt as _; + let root = Path::new("/synthetic"); + let parent = root.join(std::ffi::OsStr::from_bytes(b"not-\xff")); + let child = parent.join("file"); + let mut marks = Marks::default(); + marks.toggle(Target { + path: child.clone(), + bytes: 1, + is_dir: false, + hidden: false, + }); + marks.toggle(Target { + path: parent.clone(), + bytes: 1, + is_dir: true, + hidden: false, + }); + assert_eq!(marks.ancestor(&child), Some(parent.as_path())); + assert_eq!(marks.absorb(&parent), 1); + assert!(marks.contains(&parent)); + assert!(!marks.contains(&child)); + } + #[test] fn toggling_marks_and_unmarks() { let mut marks = Marks::default(); @@ -176,47 +618,35 @@ mod tests { marks.toggle(target("/home/tobi/.cache", 0)); marks.toggle(target("/home/tobi/gone", 500)); - marks.refresh(root_path, &root, Metric::Bytes); - assert_eq!(marks.items()[0].bytes, 900); - assert!(marks.items()[0].is_dir); - assert!(marks.items()[0].hidden, "the .cache mark is hidden"); - assert_eq!(marks.items()[1].bytes, 0, "a path that no longer exists"); + // Removal bytes stay measured bytes even when the treemap shows files. + for metric in [Metric::Bytes, Metric::Files] { + marks.refresh(root_path, &root, metric); + assert_eq!(marks.items()[0].bytes, 900); + assert!(marks.items()[0].is_dir); + assert!(marks.items()[0].hidden, "the .cache mark is hidden"); + assert_eq!( + marks.items()[1].bytes, + 0, + "a path that no longer exists" + ); + } } #[test] fn find_matches_whole_components_only() { let root = tree(); + let mut resolver = Resolver::new(Path::new("/home/tobi"), &root); + assert!(resolver.resolve(Path::new("/home/tobi/.cache")).is_some()); assert!( - find( - Path::new("/home/tobi"), - &root, - Path::new("/home/tobi/.cache") - ) - .is_some() - ); - assert!( - find( - Path::new("/home/tobi"), - &root, - Path::new("/home/tobi/.cache/blob.bin") - ) - .is_some() + resolver + .resolve(Path::new("/home/tobi/.cache/blob.bin")) + .is_some() ); + assert!(resolver.resolve(Path::new("/home/tobi/cache")).is_none()); assert!( - find( - Path::new("/home/tobi"), - &root, - Path::new("/home/tobi/cache") - ) - .is_none() - ); - assert!( - find( - Path::new("/elsewhere"), - &root, - Path::new("/home/tobi/notes.bin") - ) - .is_none() + Resolver::new(Path::new("/elsewhere"), &root) + .resolve(Path::new("/home/tobi/notes.bin")) + .is_none() ); } @@ -232,4 +662,63 @@ mod tests { assert_eq!(display_path(Path::new("/var/log"), Some(home)), "/var/log"); assert_eq!(display_path(Path::new("/var/log"), None), "/var/log"); } + /// Vary each input separately: files, marks, path depth and refreshes + /// must not be conflated with growing siblings and marks together. + #[test] + fn refresh_dimension_budget() { + let measure = |folders: usize, + marked: usize, + depth: usize, + repeats: usize| { + let root_path = std::env::temp_dir().join("marks-dimensions"); + let mut root = Node::directory("root"); + let mut marks = Marks::default(); + for index in 0..folders { + let name = format!("folder-{index:05}"); + let mut child = file("payload", 1); + let mut relative = PathBuf::from(&name); + for _ in 1..depth { + let mut parent = Node::directory("nested"); + parent.children.push(child); + child = parent; + relative.push("nested"); + } + let mut folder = Node::directory(name); + folder.children.push(child); + root.children.push(folder); + relative.push("payload"); + if index >= folders - marked { + marks.toggle(Target { + path: root_path.join(relative), + bytes: 0, + is_dir: false, + hidden: false, + }); + } + } + work::reset(); + let start = std::time::Instant::now(); + for _ in 0..repeats { + marks.refresh(&root_path, &root, Metric::Bytes); + } + let operations = work::RESOLUTION.get(); + assert!(marks.items().iter().all(|item| item.bytes == 1)); + eprintln!( + "dimensions folders={folders} M={marked} D={} R={repeats} work={operations} wall={:?}", + depth + 1, + start.elapsed() + ); + operations + }; + let base = measure(128, 8, 1, 1); + let more_files = measure(1024, 8, 1, 1); + assert!(more_files <= base * 16); + let more_marks = measure(1024, 64, 1, 1); + assert!(more_marks <= more_files * 16); + let more_depth = measure(128, 8, 15, 1); + assert!(more_depth <= base * 16); + let more_refreshes = measure(128, 8, 1, 8); + assert_eq!(more_refreshes, base * 8); + assert!(more_files <= 1024 * 8 * 4); + } } diff --git a/crates/disktree-app/src/state.rs b/crates/disktree-app/src/state.rs index efb9730a..9b635606 100644 --- a/crates/disktree-app/src/state.rs +++ b/crates/disktree-app/src/state.rs @@ -2130,25 +2130,14 @@ impl Disktree { let path = target.path.clone(); if self.marks.toggle(target) { self.space_baseline = self.space_baseline.or(self.space); - let inside: Vec = self - .marks - .items() - .iter() - .filter(|item| { - item.path != path && item.path.starts_with(&path) - }) - .map(|item| item.path.clone()) - .collect(); - for inner in &inside { - self.marks.remove(inner); - } - if !inside.is_empty() { + let inside = self.marks.absorb(&path); + if inside > 0 { self.notice = Some(( format!( "{} now covers {} mark{} inside it", display_path(&path, self.home.as_deref()), - inside.len(), - if inside.len() == 1 { "" } else { "s" } + inside, + if inside == 1 { "" } else { "s" } ), Status::Neutral, )); @@ -2159,12 +2148,7 @@ impl Disktree { /// The marked directory `path` is inside, if any; never `path` itself. pub fn marked_ancestor(&self, path: &Path) -> Option { - self.marks - .items() - .iter() - .filter(|item| item.path != path && path.starts_with(&item.path)) - .map(|item| item.path.clone()) - .min_by_key(|ancestor| ancestor.as_os_str().len()) + self.marks.ancestor(path).map(Path::to_path_buf) } pub fn target_at(&self, crumbs: &[usize]) -> Option { @@ -2191,6 +2175,8 @@ impl Disktree { /// The plan the review screen shows and the removal runs. pub fn plan(&self) -> Plan { + #[cfg(test)] + crate::marks::work::PLANS.set(crate::marks::work::PLANS.get() + 1); if self.remote.is_some() { // Never judge remote paths against this machine's filesystem. // This is an export selection, not an executable removal plan. @@ -2221,19 +2207,17 @@ impl Disktree { /// to draw, and the marked/hidden/selected state of every tile is decided /// here, where the tree and the marks are both at hand. pub fn prepare(&mut self) -> Mosaic { + #[cfg(test)] + crate::marks::work::FRAMES.set(crate::marks::work::FRAMES.get() + 1); let metric = self.options.metric; let view = self.view; let hovered = self.hovered.clone(); let selected = self.selected.clone(); - // Marks are paths; the mosaic thinks in crumbs. Resolve once per frame - // rather than building a path for every tile. - let mut marked: FxHashSet> = FxHashSet::default(); - for item in self.marks.items() { - if let Some(crumbs) = self.crumbs_for_path(&item.path) { - marked.insert(crumbs); - } - } + let marked = self.tree.as_ref().map_or_else( + || Arc::new(FxHashSet::default()), + |tree| self.marks.crumbs(&self.root_path, tree, metric), + ); let Some(tiles) = self.layout().map(<[Tile]>::to_vec) else { return Mosaic { @@ -2249,6 +2233,8 @@ impl Disktree { let mut decorations = Vec::with_capacity(tiles.len()); let mut labels = Vec::new(); for tile in &tiles { + #[cfg(test)] + crate::marks::work::TILES.set(crate::marks::work::TILES.get() + 1); let crumbs = tile.crumbs(); let node = match &tile.kind { TileKind::Node { crumbs } => self.node_at(crumbs), @@ -2356,10 +2342,11 @@ impl Disktree { let mut crumbs = Vec::new(); for component in relative.components() { let name = component.as_os_str().to_string_lossy(); - let index = node - .children - .iter() - .position(|child| child.name.as_ref() == name)?; + let index = node.children.iter().position(|child| { + #[cfg(test)] + crate::marks::work::step(); + child.name.as_ref() == name + })?; crumbs.push(index); node = node.child(index)?; } @@ -3713,6 +3700,8 @@ impl Render for Disktree { window: &mut Window, cx: &mut Context<'_, Self>, ) -> impl gpui_kit::IntoElement { + #[cfg(test)] + crate::marks::work::RENDERS.set(crate::marks::work::RENDERS.get() + 1); self.rem = window.rem_size().as_f32(); self.tick_transition(window); // The titlebar names the directory on screen, however it got there: diff --git a/crates/disktree-app/src/tests.rs b/crates/disktree-app/src/tests.rs index 0520e4cb..c5cd4df1 100644 --- a/crates/disktree-app/src/tests.rs +++ b/crates/disktree-app/src/tests.rs @@ -428,6 +428,10 @@ fn remote_root_acceptance_never_stats_the_path_locally( ) { cx.update(gpui_omarchy::init); let temp = fixture(); + // A missing native absolute path exercises the no-local-stat promise on + // Windows too, where a Unix root has no drive prefix. + let remote_root = temp.path().join("only-on-the-remote-host"); + assert!(!remote_root.exists()); let (view, cx) = view_over(temp.path(), cx); update(&view, cx, |app, cx| { let crumbs = app.crumbs_for_path(&temp.path().join("junk")).unwrap(); @@ -440,7 +444,7 @@ fn remote_root_acceptance_never_stats_the_path_locally( app.finish_volume_listing( Ok(( vec![Volume { - point: PathBuf::from("/only-on-the-remote-host"), + point: remote_root.clone(), device: Some("/dev/remote-disk".into()), space: None, }], @@ -453,7 +457,7 @@ fn remote_root_acceptance_never_stats_the_path_locally( update(&view, cx, |app, cx| { app.choose_volume(cx); assert_eq!(app.remote.as_deref(), Some("-test-fixture")); - assert_eq!(app.root_path, PathBuf::from("/only-on-the-remote-host")); + assert_eq!(app.root_path, remote_root); assert!(app.marks.is_empty() && app.remote_identity.is_none()); assert!( app.tree.is_none() @@ -1031,7 +1035,11 @@ fn enter_in_the_focused_volume_picker_scans_the_selected_root( assert!(!read(&view, cx, Disktree::can_start_over)); press(cx, "enter"); assert!(!read(&view, cx, |app| app.volumes_open)); - assert_eq!(read(&view, cx, |app| app.root_path.clone()), target); + // The picker resolves symlinks and Windows short names before scanning. + assert_eq!( + read(&view, cx, |app| app.root_path.clone()), + dunce::canonicalize(&target).unwrap() + ); finish_scan(&view, cx); assert!(read(&view, cx, |app| app.tree().is_some())); } @@ -2357,3 +2365,527 @@ fn the_power_menu_offers_only_what_the_cpus_can_tell_apart( draw(cx); assert!(read(&view, cx, |app| app.power_menu.is_none())); } + +/// Exercise prepare through actual window draws, including repeated renders. +#[gpui_kit::test] +fn marked_window_resolution_budget(cx: &mut TestAppContext) { + use crate::marks::work; + use disktree_core::removal::Target; + use disktree_core::tree::{Metric, Node, NodeKind, aggregate}; + + cx.update(gpui_omarchy::init); + let mut measure = |count| { + let temp = tempfile::tempdir().expect("tempdir"); + let root_path = temp.path().to_path_buf(); + let mut tree = Node::directory("root"); + for index in 0..count { + let mut folder = Node::directory(format!("folder-{index:05}")); + folder + .children + .push(Node::entry("payload", NodeKind::File, 1)); + tree.children.push(folder); + } + aggregate(&mut tree, Metric::Bytes); + let (view, window) = cx.add_window_view(move |_, cx| { + Disktree::with_tree(root_path.clone(), tree, options(), 3, cx) + }); + update(&view, window, |app, _| { + for index in 0..count { + app.marks.toggle(Target { + path: temp + .path() + .join(format!("folder-{index:05}")) + .join("payload"), + bytes: 1, + is_dir: false, + hidden: false, + }); + } + }); + work::reset(); + let start = std::time::Instant::now(); + for _ in 0..4 { + update(&view, window, |_, cx| cx.notify()); + draw(window); + } + let elapsed = start.elapsed(); + let frames = work::FRAMES.get(); + let operations = work::RESOLUTION.get(); + let tiles = work::TILES.get(); + assert!(tiles > 0 && tiles <= count * 4 * frames); + work::reset(); + let warm_start = std::time::Instant::now(); + for _ in 0..4 { + update(&view, window, |_, cx| cx.notify()); + draw(window); + } + let warm_elapsed = warm_start.elapsed(); + let warm_frames = work::FRAMES.get(); + let warm_work = work::RESOLUTION.get(); + assert!(warm_frames >= 4); + assert!( + warm_work <= 64 * warm_frames, + "unchanged renders must not resolve all marks: {warm_work}" + ); + assert_eq!( + work::TILES.get() * frames, + tiles * warm_frames, + "visible tile work remains linear in frame count" + ); + eprintln!( + "warm window M={count} prepares={warm_frames} resolution={warm_work} tiles={} wall={warm_elapsed:?}", + work::TILES.get() + ); + window.update(|window, _| window.remove_window()); + assert!(frames >= 4, "budget must cover actual renders: {frames}"); + assert!(operations > 0, "resolution instrumentation must run"); + eprintln!( + "window M={count} N={} draws=4 prepares={frames} work={operations} wall={elapsed:?}", + count * 2 + ); + (operations, frames) + }; + let (small, small_frames) = measure(128); + let (large, large_frames) = measure(1024); + assert!( + large * small_frames <= small * large_frames * 16, + "8x marks/folders per frame: {small}/{small_frames} -> {large}/{large_frames}" + ); + assert!( + large <= 1024 * large_frames * 16, + "absolute work cap: {large}" + ); +} + +#[gpui_kit::test] +fn marked_tiles_follow_metric_rescan_and_mark_changes(cx: &mut TestAppContext) { + use disktree_core::removal::Target; + use std::sync::Arc; + + fn marked_paths(app: &mut Disktree) -> Vec { + let mosaic = app.prepare(); + let tiles = app.layout().expect("layout").to_vec(); + tiles + .iter() + .zip(mosaic.tiles) + .filter_map(|(tile, deco)| { + deco.marked.then(|| app.path_at(tile.crumbs())).flatten() + }) + .collect() + } + + cx.update(gpui_omarchy::init); + let temp = fixture(); + let (view, window) = view_over(temp.path(), cx); + let junk = temp.path().join("junk"); + update(&view, window, |app, cx| { + app.marks.toggle(Target { + path: junk.clone(), + bytes: 300_000, + is_dir: true, + hidden: false, + }); + cx.notify(); + }); + draw(window); + assert_eq!( + update(&view, window, |app, _| marked_paths(app)), + std::slice::from_ref(&junk) + ); + update(&view, window, Disktree::toggle_metric); + draw(window); + assert_eq!( + update(&view, window, |app, _| marked_paths(app)), + std::slice::from_ref(&junk) + ); + update(&view, window, |app, cx| { + let mut tree = (**app.tree.as_ref().expect("tree")).clone(); + tree.children.reverse(); + app.tree = Some(Arc::new(tree)); + cx.notify(); + }); + draw(window); + assert_eq!( + update(&view, window, |app, _| marked_paths(app)), + std::slice::from_ref(&junk) + ); + update(&view, window, |app, cx| app.unmark(&junk, cx)); + draw(window); + assert!(update(&view, window, |app, _| marked_paths(app)).is_empty()); +} + +/// Opt-in latency evidence, separate from deterministic CI work budgets. +#[gpui_kit::test] +#[ignore = "manual interaction timing on synthetic temporary files"] +fn marked_interaction_timings(cx: &mut TestAppContext) { + use crate::marks::work; + use disktree_core::removal::Target; + use gpui_kit::Modifiers; + + fn measure( + view: &Entity, + window: &mut Window, + action: &str, + repeat: usize, + run: impl FnOnce(&mut Window), + ) { + let before = read(view, window, |app| app.marks.len()); + work::reset(); + let start = std::time::Instant::now(); + run(window); + let elapsed = start.elapsed(); + let after = read(view, window, |app| app.marks.len()); + assert!(work::RENDERS.get() > 0, "must render the real screen"); + assert!(work::PLANS.get() > 0, "must include removal projections"); + assert_eq!(work::REFRESHES.get(), 0, "interaction must not rescan"); + eprintln!( + "interaction action={action} repeat={repeat} marks={before}->{after} renders={} prepares={} plans={} refreshes={} wall={elapsed:?}", + work::RENDERS.get(), + work::FRAMES.get(), + work::PLANS.get(), + work::REFRESHES.get(), + ); + } + + cx.update(gpui_omarchy::init); + let temp = tempfile::tempdir().expect("tempdir"); + // One broad directory, kept fixed while only the number of marks grows. + // Each marked folder contains a file: descendants still make one mark. + for index in 0..1025 { + let folder = temp.path().join(format!("folder-{index:05}")); + std::fs::create_dir(&folder).expect("mkdir"); + std::fs::write(folder.join("payload"), [b'x'; 64]).expect("write"); + } + let tree = scan(temp.path(), options()).expect("scan private fixture"); + let extra_path = temp.path().join("folder-01024"); + for count in [0, 1, 8, 32, 128, 1024] { + let root_path = temp.path().to_path_buf(); + let snapshot = tree.clone(); + let (view, window) = cx.add_window_view(move |_, cx| { + Disktree::with_tree(root_path.clone(), snapshot, options(), 3, cx) + }); + window.simulate_resize(gpui_kit::size(px(1400.), px(900.))); + let focus = read(&view, window, |app| app.focus.clone()); + window.update(|window, cx| window.focus(&focus, cx)); + update(&view, window, |app, cx| { + assert!(app.space.is_some() && app.show_selection); + for index in 0..count { + app.marks.toggle(Target { + path: temp.path().join(format!("folder-{index:05}")), + bytes: 64, + is_dir: true, + hidden: false, + }); + } + cx.notify(); + }); + draw(window); + for repeat in 1..=3 { + update(&view, window, |app, cx| { + app.pointer_active = false; + app.hovered = None; + app.pointer = None; + let crumbs = + app.crumbs_for_path(&extra_path).expect("extra folder"); + app.select(Some(crumbs), cx); + }); + draw(window); + measure(&view, window, "mark-space", repeat, |window| { + press(window, "space"); + }); + assert_eq!(read(&view, window, |app| app.marks.len()), count + 1); + let at = update(&view, window, |app, _| { + let tile = app + .layout() + .expect("layout") + .iter() + .find(|tile| { + matches!( + tile.kind, + disktree_core::treemap::TileKind::Node { .. } + ) + }) + .expect("node tile") + .rect; + let rect = app.view.project(tile); + let origin = app.treemap_origin.get(); + Point::new( + origin.x + px(rect.x + rect.w / 2.), + origin.y + px(rect.y + rect.h / 2.), + ) + }); + measure(&view, window, "hover", repeat, |window| { + window.simulate_mouse_move(at, None, Modifiers::none()); + draw(window); + }); + assert!(read(&view, window, |app| app.hovered.is_some())); + measure(&view, window, "review-c", repeat, |window| { + press(window, "c"); + }); + assert_eq!(read(&view, window, |app| app.screen), Screen::Review); + press(window, "escape"); + update(&view, window, |app, cx| { + app.pointer_active = false; + app.hovered = None; + app.pointer = None; + let crumbs = + app.crumbs_for_path(&extra_path).expect("extra folder"); + app.select(Some(crumbs), cx); + }); + draw(window); + press(window, "space"); + assert_eq!(read(&view, window, |app| app.marks.len()), count); + } + window.update(|window, _| window.remove_window()); + } +} + +/// The disk root is private and empty; only the displayed tree is synthetic. +fn queue_budget_view( + count: usize, + cx: &mut TestAppContext, +) -> (tempfile::TempDir, Entity, &mut Window) { + use disktree_core::tree::{Metric, Node, NodeKind, aggregate}; + + let temp = tempfile::tempdir().expect("tempdir"); + let root_path = temp.path().to_path_buf(); + let mut tree = Node::directory("root"); + let mut parent = Node::directory("container"); + for index in 0..count { + parent.children.push(Node::entry( + format!("file-{index:05}"), + NodeKind::File, + 1, + )); + } + tree.children.push(parent); + tree.children.push(Node::entry("keep", NodeKind::File, 1)); + aggregate(&mut tree, Metric::Bytes); + let (view, window) = cx.add_window_view(move |_, cx| { + Disktree::with_tree(root_path.clone(), tree, options(), 3, cx) + }); + (temp, view, window) +} + +#[gpui_kit::test] +fn queue_construction_budget(cx: &mut TestAppContext) { + use crate::marks::work; + + cx.update(gpui_omarchy::init); + let mut measure = |count| { + let (_temp, view, window) = queue_budget_view(count, cx); + let operations = update(&view, window, |app, cx| { + work::reset(); + let start = std::time::Instant::now(); + for index in 0..count { + app.toggle_mark(&[0, index], cx); + } + assert_eq!(app.marks.len(), count); + assert_eq!( + work::FRAMES.get(), + 0, + "measure insertion before rendering" + ); + let operations = work::MARKING.get() + work::COMPACTION.get(); + assert!(operations > 0); + eprintln!( + "queue-construction M={count} work={operations} wall={:?}", + start.elapsed() + ); + operations + }); + window.update(|window, _| window.remove_window()); + operations + }; + let small = measure(128); + let large = measure(1024); + assert!(large <= small * 16, "8x marks: {small} -> {large}"); + assert!(large <= 1024 * 32, "absolute insertion budget: {large}"); +} + +#[gpui_kit::test] +fn parent_absorption_budget(cx: &mut TestAppContext) { + use crate::marks::work; + + cx.update(gpui_omarchy::init); + let mut measure = |count| { + let (temp, view, window) = queue_budget_view(count, cx); + let operations = update(&view, window, |app, cx| { + let keep = app.target_at(&[1]).expect("neighbour"); + app.marks.toggle(keep.clone()); + for index in 0..count { + let target = app.target_at(&[0, index]).expect("file"); + app.marks.toggle(target); + } + work::reset(); + let start = std::time::Instant::now(); + app.toggle_mark(&[0], cx); + assert_eq!(app.marks.len(), 2); + assert_eq!(app.marks.items()[0], keep, "neighbour stays in order"); + assert_eq!( + app.marks.items()[1].path, + temp.path().join("container") + ); + assert_eq!(app.marks.items()[1].bytes, count as u64); + let operations = work::MARKING.get() + work::COMPACTION.get(); + assert!(operations > 0); + eprintln!( + "parent-absorption M={count} work={operations} compactions={} wall={:?}", + work::COMPACTION.get(), + start.elapsed() + ); + operations + }); + window.update(|window, _| window.remove_window()); + operations + }; + let small = measure(128); + let large = measure(1024); + assert!(large <= small * 16, "8x descendants: {small} -> {large}"); + assert!(large <= 1024 * 32, "absolute absorption budget: {large}"); +} + +fn review_status_work( + count: usize, + covered: bool, + cx: &mut TestAppContext, +) -> (usize, usize) { + use crate::marks::work; + use disktree_core::removal::Target; + + let (temp, view, window) = queue_budget_view(count, cx); + update(&view, window, |app, _| { + if covered { + let parent = app.target_at(&[0]).expect("parent"); + app.marks.toggle(parent); + } + for index in 0..count { + let target = if covered { + app.target_at(&[0, index]).expect("file") + } else { + Target { + path: temp + .path() + .with_extension("outside") + .join(format!("file-{index:05}")), + bytes: 1, + is_dir: false, + hidden: false, + } + }; + app.marks.toggle(target); + } + app.screen = Screen::Review; + }); + work::reset(); + let start = std::time::Instant::now(); + for _ in 0..4 { + update(&view, window, |_, cx| cx.notify()); + draw(window); + } + let elapsed = start.elapsed(); + let frames = work::RENDERS.get(); + let operations = work::REVIEW.get(); + assert!( + frames >= 4 && work::PLANS.get() >= 4, + "must render and plan Review" + ); + let rows = (count + usize::from(covered)).min(1200); + assert_eq!( + work::REVIEW_ROWS.get(), + rows * frames, + "retain the display cap" + ); + assert_eq!( + work::BLOCKED_ROWS.get(), + if covered { 0 } else { rows * frames } + ); + assert_eq!( + work::COVERED_ROWS.get(), + if covered { (rows - 1) * frames } else { 0 } + ); + assert!(operations > 0); + eprintln!( + "review-status covered={covered} M={count} work={operations} renders={frames} rows={rows} per-render={} wall={elapsed:?}", + operations / frames + ); + window.update(|window, _| window.remove_window()); + (operations, frames) +} + +fn review_status_budget(cx: &mut TestAppContext, covered: bool) { + let (small, small_frames) = review_status_work(128, covered, cx); + let (large, large_frames) = review_status_work(1024, covered, cx); + assert!( + large * small_frames <= small * large_frames * 16, + "8x status rows: {small}/{small_frames} -> {large}/{large_frames}" + ); + assert!( + large <= 1024 * large_frames * 8, + "absolute row-status budget: {large}" + ); + let (capped, capped_frames) = review_status_work(8192, covered, cx); + assert!( + capped <= 8192 * capped_frames * 8, + "status budget above row cap: {capped}" + ); +} + +#[gpui_kit::test] +fn blocked_review_status_budget(cx: &mut TestAppContext) { + cx.update(gpui_omarchy::init); + review_status_budget(cx, false); +} + +#[gpui_kit::test] +fn covered_review_status_budget(cx: &mut TestAppContext) { + cx.update(gpui_omarchy::init); + review_status_budget(cx, true); +} + +#[gpui_kit::test] +fn folder_queue_construction_budget(cx: &mut TestAppContext) { + use crate::marks::work; + use disktree_core::tree::{Node, NodeKind, aggregate}; + use std::sync::Arc; + + cx.update(gpui_omarchy::init); + let mut measure = |count| { + let (_temp, view, window) = queue_budget_view(count, cx); + let operations = update(&view, window, |app, cx| { + let metric = app.options.metric; + let tree = Arc::make_mut(app.tree.as_mut().expect("tree")); + for child in &mut tree.children[0].children { + child.kind = NodeKind::Directory; + child + .children + .push(Node::entry("payload", NodeKind::File, 1)); + } + aggregate(tree, metric); + work::reset(); + let start = std::time::Instant::now(); + for index in 0..count { + app.toggle_mark(&[0, index], cx); + } + assert_eq!(app.marks.len(), count); + assert!(app.marks.items().iter().all(|item| item.is_dir)); + assert_eq!(work::FRAMES.get(), 0); + let operations = work::MARKING.get() + work::COMPACTION.get(); + assert!(operations > 0); + eprintln!( + "folder-queue M={count} work={operations} wall={:?}", + start.elapsed() + ); + operations + }); + window.update(|window, _| window.remove_window()); + operations + }; + let small = measure(128); + let large = measure(1024); + assert!(large <= small * 16, "8x folders: {small} -> {large}"); + assert!( + large <= 1024 * 32, + "absolute folder insertion budget: {large}" + ); +} diff --git a/crates/disktree-app/src/views.rs b/crates/disktree-app/src/views.rs index 32aedf5f..bc171cfd 100644 --- a/crates/disktree-app/src/views.rs +++ b/crates/disktree-app/src/views.rs @@ -4,6 +4,8 @@ //! always exactly what the state says — there is no second copy of anything to //! keep in sync. +use std::path::Path; + use disktree_core::classify::Category; use disktree_core::insights::{Candidate, Finding, STALE_DAYS}; use disktree_core::removal::{Plan, RemovalMode, Target}; @@ -24,6 +26,7 @@ use gpui_omarchy::{ }; use gpui_kit::prelude::FluentBuilder as _; +use rustc_hash::{FxHashMap, FxHashSet}; use crate::palette; use crate::state::{ @@ -37,6 +40,54 @@ use crate::widgets; /// removed; the list only stops being exhaustive, which it says out loud. const LIST_LIMIT: usize = 1200; +/// Row decoration borrows this render's freshly evaluated plan. It must +/// not cache removal authorization, and each displayed row needs constant +/// expected hash lookups rather than scans of all blocked/covered targets. +#[derive(Debug)] +struct ReviewStatus<'a> { + covered: FxHashSet<&'a Path>, + blocked: FxHashMap<&'a Path, &'a str>, +} + +impl<'a> ReviewStatus<'a> { + fn new(plan: &'a Plan) -> Self { + let mut status = Self { + covered: FxHashSet::default(), + blocked: FxHashMap::default(), + }; + for target in &plan.covered { + #[cfg(test)] + crate::marks::work::REVIEW + .set(crate::marks::work::REVIEW.get() + 1); + status.covered.insert(target.path.as_path()); + } + for target in &plan.blocked { + #[cfg(test)] + crate::marks::work::REVIEW + .set(crate::marks::work::REVIEW.get() + 1); + // A raw queue may repeat a path. Keep the first explanation, + // matching the original linear find's behavior. + status + .blocked + .entry(target.path.as_path()) + .or_insert(target.reason.as_str()); + } + status + } + + fn covered(&self, path: &Path) -> bool { + #[cfg(test)] + crate::marks::work::REVIEW.set(crate::marks::work::REVIEW.get() + 1); + self.covered.contains(path) + } + + fn blocked(&self, path: &Path) -> Option<&str> { + #[cfg(test)] + crate::marks::work::REVIEW.set(crate::marks::work::REVIEW.get() + 1); + self.blocked.get(path).copied() + } +} + /// The whole window. pub fn root( app: &mut Disktree, @@ -2754,9 +2805,8 @@ fn review( ) -> Div { let theme = cx.omarchy().clone(); let plan = app.plan(); - let items: Vec = app.marks.items().to_vec(); - let covered: Vec = plan.covered.clone(); - let blocked = plan.blocked.clone(); + let items = app.marks.items(); + let status = ReviewStatus::new(&plan); let mut list = div() .id("marked-list") @@ -2779,12 +2829,19 @@ fn review( } for (index, item) in items.iter().take(LIST_LIMIT).enumerate() { - let is_covered = - covered.iter().any(|covered| covered.path == item.path); - let blocked_reason = blocked - .iter() - .find(|blocked| blocked.path == item.path) - .map(|blocked| blocked.reason.clone()); + let is_covered = status.covered(&item.path); + let blocked_reason = status.blocked(&item.path).map(str::to_owned); + #[cfg(test)] + { + use crate::marks::work; + work::REVIEW_ROWS.set(work::REVIEW_ROWS.get() + 1); + work::COVERED_ROWS + .set(work::COVERED_ROWS.get() + usize::from(is_covered)); + work::BLOCKED_ROWS.set( + work::BLOCKED_ROWS.get() + + usize::from(blocked_reason.is_some()), + ); + } list = list.child(mark_row( index, item, @@ -3911,3 +3968,64 @@ fn help_overlay(app: &Disktree, cx: &gpui_kit::App) -> Div { ), ) } + +#[cfg(test)] +mod tests { + use super::*; + use disktree_core::removal::Blocked; + use std::path::PathBuf; + + #[test] + fn review_status_preserves_exact_paths_and_first_blocked_reason() { + let path = PathBuf::from("/synthetic/a/child"); + let mut plan = Plan::default(); + plan.covered.push(Target { + path: path.clone(), + bytes: 1, + is_dir: false, + hidden: false, + }); + plan.blocked.push(Blocked { + path: path.clone(), + reason: "first".into(), + }); + plan.blocked.push(Blocked { + path: path.clone(), + reason: "second".into(), + }); + let status = ReviewStatus::new(&plan); + assert!(status.covered(&path)); + assert!(!status.covered(Path::new("/synthetic/a/childish"))); + assert!(!status.covered(Path::new("/synthetic/A/child"))); + assert_eq!(status.blocked(&path), Some("first")); + assert_eq!( + status.blocked(Path::new("/synthetic/a/./child")), + Some("first") + ); + assert_eq!(status.blocked(Path::new("/synthetic/A/child")), None); + plan.blocked.clear(); + plan.covered.clear(); + let refreshed = ReviewStatus::new(&plan); + assert_eq!(refreshed.blocked(&path), None); + assert!(!refreshed.covered(&path)); + } + + #[cfg(unix)] + #[test] + fn review_status_keeps_non_utf8_paths_distinct() { + use std::os::unix::ffi::OsStrExt as _; + let path = Path::new("/synthetic") + .join(std::ffi::OsStr::from_bytes(b"not-\xff")); + let mut plan = Plan::default(); + plan.blocked.push(Blocked { + path: path.clone(), + reason: "blocked".into(), + }); + let status = ReviewStatus::new(&plan); + assert_eq!(status.blocked(&path), Some("blocked")); + assert_eq!( + status.blocked(Path::new(&path.to_string_lossy().into_owned())), + None + ); + } +} diff --git a/crates/disktree-core/src/remote.rs b/crates/disktree-core/src/remote.rs index 27da007f..7e6e60ff 100644 --- a/crates/disktree-core/src/remote.rs +++ b/crates/disktree-core/src/remote.rs @@ -577,7 +577,7 @@ impl RemoteScan { Self::spawn_with_archive(command, host, request, archive) } - #[cfg(test)] + #[cfg(all(test, unix))] fn spawn_command( command: Command, host: &str, @@ -744,6 +744,7 @@ mod tests { } } + #[cfg(unix)] fn wait(scan: &RemoteJob) -> io::Result { wait_for(scan, std::time::Duration::from_secs(10)) } @@ -1209,18 +1210,25 @@ mod tests { .unwrap(); assert_eq!(snapshot.tree.bytes, if dedup { 10 } else { 15 }); assert_eq!(snapshot.tree.files, 3); - let preview = shared.lock().unwrap().preview.clone().unwrap(); - let nested = preview - .entries - .iter() - .find(|entry| entry.name == "nested") - .unwrap(); - assert_eq!(nested.bytes, 5); - assert_eq!(nested.files, 1); - assert_eq!( - preview.entries.iter().map(|e| e.bytes).sum::(), - snapshot.tree.bytes - ); + // Only the Unix walker emits the directory events needed for a + // live preview. The Windows fallback sends a completed tree. + #[cfg(unix)] + { + let preview = shared.lock().unwrap().preview.clone().unwrap(); + let nested = preview + .entries + .iter() + .find(|entry| entry.name == "nested") + .unwrap(); + assert_eq!(nested.bytes, 5); + assert_eq!(nested.files, 1); + assert_eq!( + preview.entries.iter().map(|e| e.bytes).sum::(), + snapshot.tree.bytes + ); + } + #[cfg(not(unix))] + assert!(shared.lock().unwrap().preview.is_none()); } } diff --git a/crates/disktree-core/src/removal.rs b/crates/disktree-core/src/removal.rs index 60277529..8c43bcd3 100644 --- a/crates/disktree-core/src/removal.rs +++ b/crates/disktree-core/src/removal.rs @@ -24,6 +24,13 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver}; use std::thread; +#[cfg(test)] +thread_local! { + static NESTING_WORK: std::cell::Cell = const { + std::cell::Cell::new(0) + }; +} + /// One path the user asked to remove. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Target { @@ -136,7 +143,7 @@ fn plan_against(targets: &[Target], root: &Path, mounts: &MountTable) -> Plan { root: root.clone(), ..Plan::default() }; - let mut accepted: Vec = Vec::new(); + let mut accepted: Vec<(PathBuf, Target)> = Vec::new(); if targets.is_empty() { return plan; @@ -159,27 +166,45 @@ fn plan_against(targets: &[Target], root: &Path, mounts: &MountTable) -> Plan { }); continue; } - accepted.push(Target { - path, - ..target.clone() - }); + accepted.push(( + guard_key(&path), + Target { + path, + ..target.clone() + }, + )); } - // A path inside another target is removed with it. Keep the outer one and - // report the inner one so the review screen can explain the nesting. - accepted.sort_by(|left, right| left.path.cmp(&right.path)); - let mut outer: Vec = Vec::new(); - for target in accepted { - if outer - .iter() - .any(|candidate| within(&target.path, &candidate.path)) - { + // Descendants are contiguous only when sorted by the same keys the + // guards compare (including case and macOS Data-volume aliases). Once + // sorted, only the last retained outer path can cover the next one. + accepted.sort_by(|left, right| { + #[cfg(test)] + NESTING_WORK.set(NESTING_WORK.get() + 1); + left.0 + .cmp(&right.0) + .then_with(|| left.1.path.cmp(&right.1.path)) + }); + let mut outer: Option = None; + for (key, target) in accepted { + #[cfg(test)] + NESTING_WORK.set(NESTING_WORK.get() + 1); + if outer.as_ref().is_some_and(|parent| key.starts_with(parent)) { plan.covered.push(target); } else { - outer.push(target); + outer = Some(key); + plan.targets.push(target); } } - plan.targets = outer; + // Preserve the review list's original path ordering, independently of + // the guard-key ordering used to establish nesting. + for targets in [&mut plan.targets, &mut plan.covered] { + targets.sort_by(|left, right| { + #[cfg(test)] + NESTING_WORK.set(NESTING_WORK.get() + 1); + left.path.cmp(&right.path) + }); + } // Which volume each target's bytes come back to. Only what comes back to // the scanned volume may be shown against its free space. @@ -279,12 +304,6 @@ fn windows_key(path: &Path) -> PathBuf { path.to_path_buf() } -/// `path` is `base` or inside it, by whole components, as the guards -/// compare them. -fn within(path: &Path, base: &Path) -> bool { - guard_key(path).starts_with(guard_key(base)) -} - /// The system trees with their guard keys, computed once: the review screen /// plans on every frame, for every mark. fn system_tree_keys() -> &'static [(String, PathBuf)] { @@ -1749,6 +1768,78 @@ mod tests { ); } + #[test] + fn removal_nesting_budget() { + let temp = TempDir::new().expect("tempdir"); + let measure = |count| { + let targets: Vec<_> = (0..count) + .map(|index| (index * 73) % count) + .map(|index| Target { + path: temp.path().join(format!("item-{index:05}")), + bytes: 1, + is_dir: false, + hidden: false, + }) + .collect(); + NESTING_WORK.set(0); + let start = std::time::Instant::now(); + let plan = + plan_against(&targets, temp.path(), &MountTable::default()); + let elapsed = start.elapsed(); + let operations = NESTING_WORK.get(); + assert_eq!(plan.targets.len(), count); + eprintln!("plan M={count} work={operations} wall={elapsed:?}"); + operations + }; + let small = measure(128); + let large = measure(1024); + assert!(large <= small * 16, "8x marks: {small} -> {large}"); + assert!(large <= 1024 * 32, "absolute work cap: {large}"); + } + + #[test] + fn nesting_keeps_component_boundaries_duplicates_and_output_order() { + let temp = TempDir::new().expect("tempdir"); + let targets: Vec<_> = + ["z", "ab", "a/deep", "a", "a", "ab/inner", "a/deeper/file"] + .map(|name| target(&temp.path().join(name), 1)) + .into(); + let result = + plan_against(&targets, temp.path(), &MountTable::default()); + let paths = |targets: &[Target]| -> Vec<_> { + targets + .iter() + .map(|item| { + item.path + .strip_prefix(temp.path()) + .expect("under root") + .to_path_buf() + }) + .collect() + }; + assert_eq!(paths(&result.targets), ["a", "ab", "z"].map(PathBuf::from)); + assert_eq!( + paths(&result.covered), + ["a", "a/deep", "a/deeper/file", "ab/inner"].map(PathBuf::from) + ); + assert!(result.blocked.is_empty()); + } + + #[cfg(any(windows, target_os = "macos"))] + #[test] + fn nesting_uses_guard_case_order_even_when_the_child_sorts_first() { + let temp = TempDir::new().expect("tempdir"); + let parent = target(&temp.path().join("alpha"), 3); + let child = target(&temp.path().join("ALPHA/child"), 1); + let result = plan_against( + &[child.clone(), parent.clone()], + temp.path(), + &MountTable::default(), + ); + assert_eq!(result.targets, [parent]); + assert_eq!(result.covered, [child]); + } + #[test] fn a_plan_keeps_only_the_outermost_targets() { let temp = tree(); diff --git a/docs/performance/mark-resolution/README.md b/docs/performance/mark-resolution/README.md new file mode 100644 index 00000000..e8d81b96 --- /dev/null +++ b/docs/performance/mark-resolution/README.md @@ -0,0 +1,227 @@ +# Large removal-queue performance + +The final combined scope and current validation are documented in +[expanded evidence](expanded/README.md): the original path-resolution and +removal-plan fixes plus efficient queue construction, one-pass parent +absorption and indexed Review status. The expanded regular budgets fail +before those additions and pass afterward, including 8,192-entry Review +queues. Current validation: 241 enabled tests and the required lint gate pass. + +A mark is one queued file or folder, regardless of how many files a marked +folder contains. The original 6.60-second stress result was four forced draws +and eight prepares of Explore; it was not one scan or one click. The expanded +report separates whole-queue construction, one parent action, four Review +draws and individual UI inputs, and includes fresh combined-head timings. + +The sections below preserve the original three fixes' measurements and +reproduction history. They are historical stage results; the expanded report +is the source for the final combined implementation and validation. + +Measured on 2026-10-14, Darwin arm64, Rust 1.97.1, repository development/test +profile (application opt-level 1, dependency opt-level 2). All trees and paths +are synthetic or inside private temporary directories. No home/volume scan, +real Trash operation, APFS accounting change or parent-branch edit was used. + +## What the timing means + +A mark is one independently queued file or directory, not every file inside a +marked directory. The 1,024-mark case is a large-queue stress test, not an +established frequent manual workload. The 6.60-second result is **four forced +draws/eight prepares of Explore with that queue already present**. It is not +one scan or one click. + +[Individual-action measurements and call-path explanation](interactions.md) +now cover 0, 1, 8, 32, 128 and 1,024 queued paths on a fixed real temporary +tree, using Space, pointer movement and Review inputs. Main already completes +those inputs in a few milliseconds with a handful of marks; the demonstrated +large improvement applies to large queues. At 1,024 queued folders, adding +one more or hovering takes about 1.1 seconds on main in that fixture. These +costs can recur on renders while interacting, rather than once per scan. + +## Broader follow-up audit + +[Additional rendering/scanning findings](additional-audit.md) document +the audit that identified queue construction, parent absorption and Review +status joins, now addressed in the expansion. Depth, mount-topology and the +case-sensitive classification edge case remain separate follow-ups. +Historical test-only probes and raw counts are included. + +## Red before green + +Base: `6c8d4ce6211bf135ff4e42890faebff1040e1846` (upstream/main and fork main). +Commit `5a3c92e` adds probes and budgets without changing the algorithms. +Both application budgets and the core removal budget fail there. Their exact +output is in [red.log](red.log) and [red-core.log](red-core.log). + +Fix: `1206fda1437c288a53c4625e9ed2f0df05a8997e`. The same budgets pass; +[green.log](green.log) also includes the additional dimension and warm-frame +checks. The baseline dimension test was temporarily applied to the red +snapshot before restoring the fixed sources; see +[red-dimensions.log](red-dimensions.log). + +The three red budgets grow marks from 128 to 1,024. Refresh and window fixtures +also grow sibling folders from 128 to 1,024, each containing one marked file: +N=256/2,048 non-root nodes, M=128/1,024 marks, D=2 path components. The removal +fixture uses disjoint paths in a deterministic permutation, so the sort does +real work and no early ancestor match hides the quadratic loop. + +| Production path | Main work at 128 / 1,024 marks | Growth | Fixed work at 128 / 1,024 marks | Growth | +| --- | ---: | ---: | ---: | ---: | +| `plan_against`, one plan | 9,187 / 537,027 | 58.5x | 1,314 / 15,298 | 11.6x | +| `Marks::refresh`, four refreshes | 34,048 / 2,107,392 | 61.9x | 2,560 / 20,480 | 8.0x | +| Real window, four forced draws | 68,312 / 4,215,000 | 61.7x | 856 / 5,336 | 6.2x | + +Every budget requires at most 16x work for 8x input, plus an absolute cap: +32 work units per mark for removal; 16 per mark per refresh for refresh; 16 per +mark per observed `prepare` for the window. No elapsed-time assertion is used. + +Removal units count sort comparisons and nesting-candidate checks. Resolution +units count mark visits and sibling-name comparisons before the fix; after the +fix they count mark visits, indexed sibling entries and component lookups. +These are logical work budgets, not CPU instruction counts. Hashing/comparing +long names and paths still costs their length. All probes are test-only. + +The GPUI harness forces notifications, draws the actual window and asserts +that `Disktree::prepare` ran at least once per draw: eight calls were observed +for four draws. This includes path lookups elsewhere in the screen, not just +the new resolver. It catches reintroducing a linear sibling search per mark +into `prepare`, even if a resolver microbenchmark still passes. + +## Wall measurements + +These are medians of three serial repetitions with `--test-threads=1` on the +same machine/profile. Fixture construction, compilation and mark insertion +are outside the timers. The window timer includes the real render path, +layout/painting work and freshly evaluated removal projections. Backing roots +are empty temporary directories, with the displayed trees constructed in +memory; these are scaling workloads, not a filesystem latency benchmark. + +| Workload at 1,024 marks | Main median | Fixed median | Speedup | +| --- | ---: | ---: | ---: | +| One removal plan | 398.13 ms | 23.24 ms | 17.1x | +| Four mark refreshes | 9.07 ms | 1.53 ms | 5.9x | +| Four window draws, eight prepares | 6,602.00 ms | 358.68 ms | 18.4x | + +Raw samples: [wall-red-repeats.log](wall-red-repeats.log) and +[wall-green-repeats.log](wall-green-repeats.log). Fixed warm draws have their +own timer and are excluded from the cold-window median above. Timings are +observations, not portable thresholds or a frame-rate guarantee. + +## Which variables actually cause the growth + +N is all scanned nodes, M is marked paths, D is path depth, R is the number +of refreshes/renders. F below is the number of first-level sibling folders. +The remaining single-child chains make depth independently controllable. + +| Folders F | Marks M | Depth D | Refreshes R | Main work | Fixed work | +| ---: | ---: | ---: | ---: | ---: | ---: | +| 128 | 8 | 2 | 1 | 1,012 | 160 | +| 1,024 | 8 | 2 | 1 | 8,180 | 1,056 | +| 1,024 | 64 | 2 | 1 | 63,648 | 1,280 | +| 128 | 8 | 16 | 1 | 1,124 | 384 | +| 128 | 8 | 2 | 8 | 8,096 | 1,280 | + +Main is linear in each of F, M, D and R when the others are held fixed. +It becomes quadratic when siblings and marks grow together. Repeating an +unchanged linear pass R times is O(items * R); calling it quadratic solely +because R can grow would misidentify the problem. Unmarked file counts do +not affect removal nesting at all, but sibling counts affect path lookup. + +On main: + +- Disjoint accepted removal marks cause M(M-1)/2 ancestor checks after sorting. +- Refresh resolves each marked path separately. At a broad directory, each + lookup scans siblings again: O(M*N) worst-case per refresh. +- `prepare` repeats analogous path-to-crumb searches for every mark on every + render, even when the tree and marks are unchanged. + +After the fix: + +- Nesting costs O(M log M) path comparisons plus M prefix checks. Guard keys + are computed once per accepted target, sorted using the guards' case and + macOS Data-volume semantics, and compared with the last retained outer + target. Display output retains the original path ordering. Including path + length L, this stage costs O(M log M * L) time and O(M * L) storage. +- One `Resolver` batch indexes each visited directory's siblings once. Let S + be those indexed entries (S <= N). Expected hash work is O(S + M*D) per + batch, with O(S) temporary index storage; untouched subtrees cost nothing. + `Marks::refresh` creates one batch per refresh, so R refreshes still cost + O(R * (S + M*D)). Path/name lengths are additional hashing/copying costs. +- `Marks::crumbs` stores O(M*D) crumbs, shared by `Arc`. An unchanged frame + reuses that set without resolving marked paths. The first frame after an + invalidation costs one batch. Visible tiles still require processing each + frame: O(T*R) at bounded display depth, where T is the laid-out tile count. + Ancestor-mark checks depend on depth; this does not claim depth-independent + work for arbitrarily deep crumb vectors. + +The real-window warm budget allows only 64 resolution units per observed +prepare, independent of M. Both mark counts measured 216 units over eight +prepares (other screen lookups); marked-path resolution was reused. It also +counts actual tile visits and verifies proportional work across repeated +frames. Merging tiny tiles means T need not grow with N. + +Fresh removal plans still evaluate guards, resolve filesystem paths and +attribute volumes for marks on every render. The sort remains O(M log M) +per plan, and mount-table/system-tree checks retain their existing costs. +These costs are visible in the remaining ~359 ms window workload. No removal +authorization is cached. The worker's fresh mount/link checks immediately +before deletion and descriptor-based deletion protections are unchanged. + +## Invalidation and semantics + +The display cache is invalidated by toggle, remove, clear and refresh, and +checks tree allocation identity, root path and metric. It stores a `Weak` +reference to the tree, so it cannot retain an old scan; the weak reference also +prevents address reuse from masquerading as the same tree. `Arc::make_mut` +dissociates that identity before in-place changes can affect a cached tree. + +Tests cover every boundary, metric reordering and replacement through the +window, missing paths, component boundaries, exact case-sensitive scan names, +first-match behavior for equal lossy names, and non-UTF-8 Unix paths. Removal +nesting uses the platform guard keys, with coverage for case variants, +duplicates, component prefixes and stable output ordering. Existing safety +and synthetic permanent-deletion tests remain green. + +Integration API: + +```rust +let mut resolver = marks::Resolver::new(root_path, tree); +let resolved = resolver.resolve(path); // Option<(Vec, &Node)> +``` + +Reuse a resolver for the whole batch. The old standalone `marks::find` was +removed when its production caller moved to this API. Display callers use +`Marks::crumbs(&Path, &Arc, Metric) -> Arc>>`; +callers have no revision-counter or manual invalidation obligation. + +## Original-stage verification and reproduction + +- `cargo xtask lint`: pass; [lint.log](lint.log). +- `cargo xtask test`: 78 app, 150 core and 3 xtask tests pass; + [tests.log](tests.log). Two pre-existing ignored core tests remain ignored. The new opt-in + interaction timing test is also ignored by the normal suite; it passes + when explicitly run on both main and fixed snapshots. +- `cargo test --workspace budget -- --nocapture`: all budgets pass. +- Three serial red repetitions fail the intended budgets (exit 101), followed + by three serial green repetitions passing them (exit 0). +- Local validation is macOS arm64. Other operating systems/architectures are + left to the repository's existing CI; no cross-platform result is claimed. + +The commit IDs in this report refer to pre-consolidation development +snapshots archived in [fork PR #9](https://github.com/huntharo/disktree/pull/9). +See the [historical fetch instructions](expanded/README.md#red-before-green). + +From a clean checkout at `5a3c92e`, reproduce the failures with: + +```sh +cargo test -p disktree-app budget -- --nocapture --test-threads=1 +cargo test -p disktree-core removal_nesting_budget -- --nocapture +``` + +At `1206fda`, run the green budgets and gates: + +```sh +cargo test --workspace budget -- --nocapture --test-threads=1 +cargo xtask lint +cargo xtask test +``` diff --git a/docs/performance/mark-resolution/additional-audit.log b/docs/performance/mark-resolution/additional-audit.log new file mode 100644 index 00000000..935b5e92 --- /dev/null +++ b/docs/performance/mark-resolution/additional-audit.log @@ -0,0 +1,50 @@ + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 6.09s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::additional_render_complexity_audit ... audit bulk-mark M=128 ancestor-visits=8128 inside-visits=8256 wall=3.146875ms +audit absorb-parent M=128 retain-visits=8384 wall=294.333µs +audit blocked-review M=128 row-checks=66048 renders=8 per-render=8256 wall=119.267584ms +audit covered-review children=128 row-checks=67072 renders=8 per-render=8384 wall=135.083208ms +audit bulk-mark M=1024 ancestor-visits=523776 inside-visits=524800 wall=199.863084ms +audit absorb-parent M=1024 retain-visits=525824 wall=14.79025ms +audit blocked-review M=1024 row-checks=4198400 renders=8 per-render=524800 wall=1.101053583s +audit covered-review children=1024 row-checks=4206592 renders=8 per-render=525824 wall=1.23678775s +audit deep-breadcrumbs D=128 prefix-walks=65024 renders=8 per-render=8128 wall=12.958166ms +audit deep-breadcrumbs D=1024 prefix-walks=4190208 renders=8 per-render=523776 wall=33.87925ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 79 filtered out; finished in 3.29s + + Running unittests src/lib.rs (target/debug/deps/disktree_core-9300e5ba20d6b25f) + +running 4 tests +test classify::tests::additional_classification_case_variant_audit ... audit case-variant-classify F=128 sibling-checks=16384 wall=84.834µs +audit case-variant-classify F=1024 sibling-checks=1048576 wall=2.682292ms +ok +test scan::tests::additional_scan_complexity_audit ... audit real-scan nested=false F=128 listed=128 aggregate-visits=129 wall=574.375µs +audit real-scan nested=false F=1024 listed=1024 aggregate-visits=1025 wall=2.407709ms +audit real-scan nested=true F=128 listed=256 aggregate-visits=257 wall=2.721ms +audit real-scan nested=true F=1024 listed=2048 aggregate-visits=2049 wall=18.88725ms +ok +test space::tests::additional_mount_complexity_audit ... audit mount-startup K=128 pairs=8128 wall=83.875µs +audit mount-startup K=1024 pairs=523776 wall=2.992958ms +ok +test treemap::tests::additional_layout_complexity_audit ... audit uncapped-squarify F=128 ratio-visits=682 wall=4.5µs +audit capped-layout F=128 ratio-visits=482 wall=44.834µs +audit uncapped-squarify F=1024 ratio-visits=12525 wall=24.375µs +audit capped-layout F=1024 ratio-visits=319 wall=21.125µs +ok + +test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 152 filtered out; finished in 0.47s + + Running unittests src/main.rs (target/debug/deps/xtask-0dc2df263f51d89e) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s + diff --git a/docs/performance/mark-resolution/additional-audit.md b/docs/performance/mark-resolution/additional-audit.md new file mode 100644 index 00000000..f915c96c --- /dev/null +++ b/docs/performance/mark-resolution/additional-audit.md @@ -0,0 +1,195 @@ +# Additional rendering and scanning complexity audit + +**Historical audit:** queue construction, parent absorption and Review status +joins below are now fixed by the [expanded PR #9](expanded/README.md). The +remaining depth, mount-topology and classification findings stay outside +that queue-focused scope. Counts in this file describe the audited snapshot, +not the final combined implementation. + +Audited on 2026-10-14 at `ab11b1fd884e950faa9902c0b3e0e870eb6815d9`: +main `6c8d4ce` plus PR #9's fixes. APFS feature code is outside this audit. +The cases below remain after the original three fixes. This follow-up records +findings; it adds no further runtime or API change. + +[Raw probe output](additional-audit.log) and the +[reproducible test-only probe patch](additional-audit.patch) preserve the +measurements. The probes drive production functions; Review and breadcrumb +measurements draw the real GPUI window. Fixtures are synthetic in-memory trees +or real private temporary trees. No real removal or whole-home scan ran. + +M = marks, B = blocked marks, C = covered marks, D = path depth, +F = entries in one directory, K = mount-table entries. All operation counts +below compare 128 with 1,024 of the named variable, an 8x increase. Counts are +deterministic; recorded wall times are single instrumented test-profile runs, +not latency budgets or production timing guarantees. + +## Confirmed follow-ups + +| Operation | Variable / measured scope | Work at 128 | Work at 1,024 | Growth | +| --- | --- | ---: | ---: | ---: | +| Build a queue by calling `toggle_mark` repeatedly | M, no renders | 16,384 mark visits | 1,048,576 | 64.0x | +| Mark a parent over marked children | M children, one action | 8,384 retain visits | 525,824 | 62.7x | +| Render Review with all paths blocked | M, per actual render | 8,256 comparisons | 524,800 | 63.6x | +| Render Review with children covered by one parent | M children, per actual render | 8,384 comparisons | 525,824 | 62.7x | +| Build deep breadcrumbs | D, per actual render | 8,128 prefix index steps | 523,776 | 64.4x | +| Detect repeated Linux mount views | K, one startup calculation | 8,128 mount-pair checks | 523,776 | 64.4x | +| Classify case variants of `node_modules` | F, one finishing pass | 16,384 sibling checks | 1,048,576 | 64.0x | + +### Queue construction and parent absorption: highest MCP relevance + +[`Disktree::toggle_mark`](../../../crates/disktree-app/src/state.rs) invokes +`marked_ancestor`, which scans all existing marks, then scans marks again for +paths inside the new target. Repeating the operation M times costs O(M^2) +even if every mark is a disjoint file and rendering is suppressed. The probe +calls the real state transition without drawing between insertions. At 1,024 +marks it measured about 200 ms with probes, versus 3 ms at 128. + +Marking a parent over K marked descendants then calls `Marks::remove` K +times. Each call compacts the ordered target vector with `retain`: +O(K*M), or O(M^2) when most marks are descendants. This is one user action, +not the cost of the parent's scan. The 1,024-child probe measured ~15 ms; +operation counts are the stronger scaling evidence. + +A future MCP batch API should normalize the entire queue once, preserving +mark ordering and path semantics. Merely suppressing redraws while looping +over the scalar marking API does not remove this cost. Parent absorption +should remove the whole descendant set with one compaction. Ancestor +membership can use indexed ancestor paths instead of scanning every mark. + +### Review: row-status joins still use repeated linear lookup + +[`views::review`](../../../crates/disktree-app/src/views.rs) runs +`covered.iter().any(...)` and `blocked.iter().find(...)` for every displayed +item. This is O(min(M, 1200) * (B+C)) in the general case. It behaves +quadratically below the 1,200-row cap when blocked/covered counts grow with +the queue; above the cap it has a bounded displayed-row factor. It is not an +unbounded quadratic pass over every file on disk. + +The ordinary accepted-path fixture in the original budget had B=C=0, so it +could not expose these joins. The new blocked fixture queues paths outside +the scanned root; guards refuse them, with no deletion attempt. The covered +fixture supplies an overlapping raw queue. Normal manual marking absorbs +such overlaps, so the latter represents a raw/programmatic queue or other +path-alias situations, rather than an ordinary manual selection sequence. + +Each test forces four Review draws and observes eight real renders. Logs +report both totals and comparisons per render. The ~1.10 s blocked and +~1.24 s covered wall samples at 1,024 are those **four-draw totals**, not +single clicks. A per-plan lookup map/set for row status would avoid repeated +searches while leaving removal guards freshly evaluated. + +### Breadcrumbs and deep filtering: depth-dependent costs + +[`Disktree::breadcrumbs`](../../../crates/disktree-app/src/state.rs) starts at +the scan root again for every successive prefix, producing D(D-1)/2 indexed +walk steps. It also clones crumb prefixes. The view later hides most of the +trail, after the full trail has already been materialized. Above-root path +ancestors are similarly copied before this display truncation. + +The probe sets current depth to 128 and 1,024 in memory and draws the actual +Explore window. Deep chains like this are an edge workload, not evidence +that a common shallow home directory suffers this cost. Walking the node +chain once and building only displayed breadcrumb targets would bound it. + +Separately, `Matches::keep` and marked-ancestor coloring hash successive crumb +prefixes. Hashing each prefix depends on its length, so these can also cost +O(D^2) per tile when the absolute path is deep and the filter/marks require +checking all ancestors. Those hash costs were source-audited, not separately +measured by this breadcrumb counter. They depend on path depth, not the +number of siblings in one folder. + +### Linux mount setup: topology-dependent, not file-count-dependent + +[`space::repeated_mounts_excluding`](../../../crates/disktree-core/src/space.rs) +compares each visible mount with previously kept mounts. The probe uses +independent synthetic devices so no early match masks K(K-1)/2 comparisons. +This runs while preparing a scan on systems with `/proc/self/mountinfo`. +The algorithm was measured on synthetic Linux mount metadata on macOS; +there was no actual Linux mount manipulation or Linux wall-time benchmark. + +Other code in that routine can scan the full mount table or exclusion list +inside these checks, so more complex topologies deserve separate bounds. +The volume picker has a similar linear search through already seen devices. +Removal guards and volume attribution also retain O(M*K) checks per plan. +None of these is quadratic in the number of files with a fixed mount table. +The simple 1,024-mount synthetic case took ~3 ms here, so this ranks below +queue construction and Review for the proposed MCP flow. + +### Directory classification: a real but unusual within-folder case + +[`classify::reclaim_of`](../../../crates/disktree-core/src/classify.rs) requests +a sibling search only for particular names: `target`, `node_modules`, and +`logs`. Ordinary folders do not trigger a sibling scan for every arbitrary +entry. That apparent generic F^2 pattern is therefore usually a false alarm. + +However, the directory-name match is ASCII case-insensitive. A case-sensitive +filesystem can contain many distinct spellings of `node_modules`, each of +which searches all siblings for an absent `package.json`. The probe creates +128/1,024 such names in memory and calls full `classify`, observing F^2 +comparisons. There are only 2^11 case variants of this particular name, so +this is a quadratic window up to a finite namespace bound, not an unbounded +quadratic algorithm for arbitrary folder sizes. These variants cannot all +coexist on a normal case-insensitive APFS volume; this is chiefly a Linux or +case-sensitive-volume edge case. It is scan finishing, not per-frame work. + +Precomputing the few manifest/sibling flags once per parent would remove the +case without adding fields to every scanned node. + +## Normal scanning and bounded layout did not show this problem + +The regular walk appends each directory entry once. Directory completion +moves completed children into their parent, and aggregation visits each node +once before sorting each folder's children. Hardlink de-duplication and +followed-directory detection use a bitmap/hash sets, not a search through all +previous files. The admission queue removes an admitted tail, avoiding a +front-removal shift per job. + +Actual fixed-one-thread scans of private temporary trees produced: + +| Fixture | Listed entries at 128 / 1,024 items | Aggregate node visits | +| --- | ---: | ---: | +| One directory of files | 128 / 1,024 | 129 / 1,025 | +| One directory of folders, one file in each | 256 / 2,048 | 257 / 2,049 | + +Those counts are linear. Sorting is O(sum(F_i log F_i)) over directories, +with additional filename/path-length and filesystem costs. This does not +prove every input or OS path is linear; it rules out a general sibling-pair +loop in the normal walk/aggregation paths exercised here. + +`treemap::squarify` recomputes worst aspect ratios across a growing row, which +is quadratic within a row. With unbounded equal-weight input, ratio-element +visits rose from 682 to 12,525 (18.4x at 8x input): superlinear, not a measured +64x file-count quadratic. The real layout caller keeps only 96 children plus +one merged tail per directory before this step. Its measured ratio work was +482 versus 319 at 128/1,024 source entries. Ranking all source entries still +costs O(F log F) on a layout cache miss; normal unchanged frames reuse layout. +The internal row rescan therefore is not currently an unbounded wide-folder +rendering blocker. + +Windows MFT enumeration builds parent indexes and sorts names rather than +repeatedly searching every sibling. One additional **unmeasured** metadata +candidate is `mft::table_layout`'s vector `numbers.contains` while collecting +extension records from the MFT's own attribute list. That can be O(E^2) in +extension-record count E on a fragmented MFT; E is not the total file count. +Windows runtime behavior was not tested in this macOS audit. + +## Reproduce without changing runtime code + +The commit IDs below are historical pre-consolidation snapshots; see the +[fetch instructions](expanded/README.md#red-before-green). + +From clean source at `ab11b1f` (or later evidence-only descendants): + +```sh +git apply docs/performance/mark-resolution/additional-audit.patch +cargo test --workspace additional_ -- --ignored --nocapture --test-threads=1 +git apply -R docs/performance/mark-resolution/additional-audit.patch +``` + +The patch adds counters gated by `cfg(test)` and ignored diagnostic tests. +All five diagnostic tests passed their correctness/call-path assertions. It +records counts rather than adding passing assertions that would enshrine +quadratic behavior. The patch was then removed: source remains byte-identical +to `ab11b1f`. The original lint/full-test result therefore still applies to +the unchanged runtime implementation. These findings need separate fixes +and corresponding failing budgets before their scaling can be called bounded. diff --git a/docs/performance/mark-resolution/additional-audit.patch b/docs/performance/mark-resolution/additional-audit.patch new file mode 100644 index 00000000..99a7e5f8 --- /dev/null +++ b/docs/performance/mark-resolution/additional-audit.patch @@ -0,0 +1,561 @@ +diff --git a/crates/disktree-app/src/marks.rs b/crates/disktree-app/src/marks.rs +index 3a86060..8d9578a 100644 +--- a/crates/disktree-app/src/marks.rs ++++ b/crates/disktree-app/src/marks.rs +@@ -18,6 +18,11 @@ pub mod work { + thread_local! { + pub static RESOLUTION: Cell = const { Cell::new(0) }; + pub static FRAMES: Cell = const { Cell::new(0) }; ++ pub static AUDIT_ROWS: Cell = const { Cell::new(0) }; ++ pub static AUDIT_RETAIN: Cell = const { Cell::new(0) }; ++ pub static AUDIT_ANCESTORS: Cell = const { Cell::new(0) }; ++ pub static AUDIT_INSIDE: Cell = const { Cell::new(0) }; ++ pub static AUDIT_CRUMBS: Cell = const { Cell::new(0) }; + pub static RENDERS: Cell = const { Cell::new(0) }; + pub static PLANS: Cell = const { Cell::new(0) }; + pub static REFRESHES: Cell = const { Cell::new(0) }; +@@ -31,6 +36,11 @@ pub mod work { + pub fn reset() { + RESOLUTION.set(0); + FRAMES.set(0); ++ AUDIT_ROWS.set(0); ++ AUDIT_RETAIN.set(0); ++ AUDIT_ANCESTORS.set(0); ++ AUDIT_INSIDE.set(0); ++ AUDIT_CRUMBS.set(0); + RENDERS.set(0); + PLANS.set(0); + REFRESHES.set(0); +@@ -91,7 +101,11 @@ impl Marks { + pub fn remove(&mut self, path: &Path) { + if self.index.remove(path) { + self.resolved = None; +- self.items.retain(|item| item.path != path); ++ self.items.retain(|item| { ++ #[cfg(test)] ++ work::AUDIT_RETAIN.set(work::AUDIT_RETAIN.get() + 1); ++ item.path != path ++ }); + } + } + +diff --git a/crates/disktree-app/src/state.rs b/crates/disktree-app/src/state.rs +index 9713e0c..ce234ec 100644 +--- a/crates/disktree-app/src/state.rs ++++ b/crates/disktree-app/src/state.rs +@@ -1120,6 +1120,9 @@ impl Disktree { + }; + let mut crumbs: Vec = Vec::new(); + for &index in &self.crumbs { ++ #[cfg(test)] ++ crate::marks::work::AUDIT_CRUMBS ++ .set(crate::marks::work::AUDIT_CRUMBS.get() + crumbs.len()); + let Some(node) = + tree.resolve(&crumbs).and_then(|node| node.child(index)) + else { +@@ -1711,6 +1714,9 @@ impl Disktree { + .items() + .iter() + .filter(|item| { ++ #[cfg(test)] ++ crate::marks::work::AUDIT_INSIDE ++ .set(crate::marks::work::AUDIT_INSIDE.get() + 1); + item.path != path && item.path.starts_with(&path) + }) + .map(|item| item.path.clone()) +@@ -1738,7 +1744,12 @@ impl Disktree { + self.marks + .items() + .iter() +- .filter(|item| item.path != path && path.starts_with(&item.path)) ++ .filter(|item| { ++ #[cfg(test)] ++ crate::marks::work::AUDIT_ANCESTORS ++ .set(crate::marks::work::AUDIT_ANCESTORS.get() + 1); ++ item.path != path && path.starts_with(&item.path) ++ }) + .map(|item| item.path.clone()) + .min_by_key(|ancestor| ancestor.as_os_str().len()) + } +diff --git a/crates/disktree-app/src/tests.rs b/crates/disktree-app/src/tests.rs +index 04bcc6f..3d08a34 100644 +--- a/crates/disktree-app/src/tests.rs ++++ b/crates/disktree-app/src/tests.rs +@@ -2118,3 +2118,141 @@ fn marked_interaction_timings(cx: &mut TestAppContext) { + window.update(|window, _| window.remove_window()); + } + } ++ ++#[gpui_kit::test] ++#[ignore = "manual additional complexity audit on synthetic state/windows"] ++fn additional_render_complexity_audit(cx: &mut TestAppContext) { ++ use crate::marks::work; ++ use disktree_core::removal::Target; ++ use disktree_core::tree::{Node, NodeKind}; ++ ++ cx.update(gpui_omarchy::init); ++ let temp = tempfile::tempdir().expect("tempdir"); ++ let scan_root = temp.path().join("scan"); ++ std::fs::create_dir(&scan_root).expect("mkdir"); ++ for count in [128, 1024] { ++ let root_path = scan_root.clone(); ++ let mut tree = Node::directory("root"); ++ let mut parent = Node::directory("container"); ++ for index in 0..count { ++ parent.children.push(Node::entry( ++ format!("file-{index}"), ++ NodeKind::File, ++ 1, ++ )); ++ } ++ tree.children.push(parent); ++ let (view, window) = cx.add_window_view(move |_, cx| { ++ Disktree::with_tree(root_path.clone(), tree, options(), 3, cx) ++ }); ++ update(&view, window, |app, cx| { ++ work::reset(); ++ let start = std::time::Instant::now(); ++ for index in 0..count { ++ app.toggle_mark(&[0, index], cx); ++ } ++ assert_eq!(app.marks.len(), count); ++ eprintln!( ++ "audit bulk-mark M={count} ancestor-visits={} inside-visits={} wall={:?}", ++ work::AUDIT_ANCESTORS.get(), ++ work::AUDIT_INSIDE.get(), ++ start.elapsed() ++ ); ++ work::reset(); ++ let start = std::time::Instant::now(); ++ app.toggle_mark(&[0], cx); ++ assert_eq!(app.marks.len(), 1); ++ assert!(app.marks.contains(&scan_root.join("container"))); ++ eprintln!( ++ "audit absorb-parent M={count} retain-visits={} wall={:?}", ++ work::AUDIT_RETAIN.get(), ++ start.elapsed() ++ ); ++ app.marks.clear(); ++ for index in 0..count { ++ app.marks.toggle(Target { ++ path: temp.path().join(format!("outside-{index}")), ++ bytes: 1, ++ is_dir: false, ++ hidden: false, ++ }); ++ } ++ app.screen = Screen::Review; ++ }); ++ work::reset(); ++ let start = std::time::Instant::now(); ++ for _ in 0..4 { ++ update(&view, window, |_, cx| cx.notify()); ++ draw(window); ++ } ++ let frames = work::RENDERS.get(); ++ assert!(frames >= 4); ++ eprintln!( ++ "audit blocked-review M={count} row-checks={} renders={frames} per-render={} wall={:?}", ++ work::AUDIT_ROWS.get(), ++ work::AUDIT_ROWS.get() / frames, ++ start.elapsed() ++ ); ++ update(&view, window, |app, cx| { ++ // Manual marking absorbs these; raw/programmatic queues can ++ // still present overlapping targets to the review renderer. ++ app.marks.clear(); ++ let parent = app.target_at(&[0]).expect("parent"); ++ app.marks.toggle(parent); ++ for index in 0..count { ++ let child = app.target_at(&[0, index]).expect("child"); ++ app.marks.toggle(child); ++ } ++ cx.notify(); ++ }); ++ work::reset(); ++ let start = std::time::Instant::now(); ++ for _ in 0..4 { ++ update(&view, window, |_, cx| cx.notify()); ++ draw(window); ++ } ++ let frames = work::RENDERS.get(); ++ assert!(frames >= 4); ++ eprintln!( ++ "audit covered-review children={count} row-checks={} renders={frames} per-render={} wall={:?}", ++ work::AUDIT_ROWS.get(), ++ work::AUDIT_ROWS.get() / frames, ++ start.elapsed() ++ ); ++ window.update(|window, _| window.remove_window()); ++ } ++ for depth in [128, 1024] { ++ let root_path = scan_root.clone(); ++ let mut tree = Node::directory("x"); ++ tree.children ++ .push(Node::entry("payload", NodeKind::File, 1)); ++ for _ in 0..depth { ++ let mut parent = Node::directory("x"); ++ parent.children.push(tree); ++ tree = parent; ++ } ++ let (view, window) = cx.add_window_view(move |_, cx| { ++ Disktree::with_tree(root_path.clone(), tree, options(), 3, cx) ++ }); ++ update(&view, window, |app, cx| { ++ app.crumbs = vec![0; depth]; ++ app.selected = None; ++ cx.notify(); ++ }); ++ work::reset(); ++ let start = std::time::Instant::now(); ++ for _ in 0..4 { ++ update(&view, window, |_, cx| cx.notify()); ++ draw(window); ++ } ++ let frames = work::RENDERS.get(); ++ assert!(frames >= 4); ++ eprintln!( ++ "audit deep-breadcrumbs D={depth} prefix-walks={} renders={frames} per-render={} wall={:?}", ++ work::AUDIT_CRUMBS.get(), ++ work::AUDIT_CRUMBS.get() / frames, ++ start.elapsed() ++ ); ++ window.update(|window, _| window.remove_window()); ++ } ++} +diff --git a/crates/disktree-app/src/views.rs b/crates/disktree-app/src/views.rs +index c23e91b..ff3acad 100644 +--- a/crates/disktree-app/src/views.rs ++++ b/crates/disktree-app/src/views.rs +@@ -2544,11 +2544,20 @@ fn review( + } + + for (index, item) in items.iter().take(LIST_LIMIT).enumerate() { +- let is_covered = +- covered.iter().any(|covered| covered.path == item.path); ++ let is_covered = covered.iter().any(|covered| { ++ #[cfg(test)] ++ crate::marks::work::AUDIT_ROWS ++ .set(crate::marks::work::AUDIT_ROWS.get() + 1); ++ covered.path == item.path ++ }); + let blocked_reason = blocked + .iter() +- .find(|blocked| blocked.path == item.path) ++ .find(|blocked| { ++ #[cfg(test)] ++ crate::marks::work::AUDIT_ROWS ++ .set(crate::marks::work::AUDIT_ROWS.get() + 1); ++ blocked.path == item.path ++ }) + .map(|blocked| blocked.reason.clone()); + list = list.child(mark_row( + index, +diff --git a/crates/disktree-core/src/classify.rs b/crates/disktree-core/src/classify.rs +index bfe24a7..82ffd9e 100644 +--- a/crates/disktree-core/src/classify.rs ++++ b/crates/disktree-core/src/classify.rs +@@ -17,6 +17,10 @@ use rayon::prelude::*; + + use crate::tree::Node; + ++#[cfg(test)] ++static AUDIT_SIBLINGS: std::sync::atomic::AtomicUsize = ++ std::sync::atomic::AtomicUsize::new(0); ++ + /// A kind of data, for colour. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] + pub enum Category { +@@ -231,8 +235,14 @@ pub fn classify(root: &mut Node) { + root.reclaim = None; + for index in 0..root.children.len() { + let siblings = &root.children; +- let has_sibling = +- |wanted: &str| siblings.iter().any(|name| &*name.name == wanted); ++ let has_sibling = |wanted: &str| { ++ siblings.iter().any(|name| { ++ #[cfg(test)] ++ AUDIT_SIBLINGS ++ .fetch_add(1, std::sync::atomic::Ordering::Relaxed); ++ &*name.name == wanted ++ }) ++ }; + let child = &siblings[index]; + // A top-level directory with an unknown name takes the kind of its + // largest recognisable child: `~/world` is mostly `.git`. +@@ -303,8 +313,13 @@ fn kind_of( + if !child.is_dir() { + return (category, reclaim); + } +- let has_sibling = +- |wanted: &str| siblings.iter().any(|name| &*name.name == wanted); ++ let has_sibling = |wanted: &str| { ++ siblings.iter().any(|name| { ++ #[cfg(test)] ++ AUDIT_SIBLINGS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); ++ &*name.name == wanted ++ }) ++ }; + let child_category = category_of_name(&child.name) + .or_else(|| is_git_store(child).then_some(Category::Git)) + .unwrap_or(category); +@@ -616,4 +631,39 @@ mod tests { + assert!(!category.label().is_empty()); + } + } ++ #[test] ++ #[ignore = "manual case-sensitive sibling classification edge-case audit"] ++ fn additional_classification_case_variant_audit() { ++ for count in [128, 1024] { ++ let mut root = Node::directory("root"); ++ for index in 0..count { ++ let mut mask = 1; ++ let name: String = "node_modules" ++ .chars() ++ .map(|letter| { ++ if letter == '_' { ++ return letter; ++ } ++ let upper = index & mask != 0; ++ mask <<= 1; ++ if upper { ++ letter.to_ascii_uppercase() ++ } else { ++ letter ++ } ++ }) ++ .collect(); ++ root.children.push(Node::directory(name)); ++ } ++ AUDIT_SIBLINGS.store(0, std::sync::atomic::Ordering::Relaxed); ++ let start = std::time::Instant::now(); ++ classify(&mut root); ++ assert!(root.children.iter().all(|child| child.reclaim.is_none())); ++ eprintln!( ++ "audit case-variant-classify F={count} sibling-checks={} wall={:?}", ++ AUDIT_SIBLINGS.load(std::sync::atomic::Ordering::Relaxed), ++ start.elapsed() ++ ); ++ } ++ } + } +diff --git a/crates/disktree-core/src/scan.rs b/crates/disktree-core/src/scan.rs +index 693253a..ab27ea7 100644 +--- a/crates/disktree-core/src/scan.rs ++++ b/crates/disktree-core/src/scan.rs +@@ -283,6 +283,9 @@ pub fn scan(root: &Path, options: ScanOptions) -> io::Result { + Ok(node) + } + ++#[cfg(test)] ++static AUDIT_CLASSIFIED: AtomicUsize = AtomicUsize::new(0); ++ + struct WalkContext { + /// A subtree to reuse rather than walk. + known: Option, +@@ -328,6 +331,8 @@ impl WalkContext { + /// files, and a disk has millions of them. The name is moved out of + /// the entry last, so nothing copies it. + fn classify(&self, dir: &Path, entry: &mut impl Listed) -> Classified { ++ #[cfg(test)] ++ AUDIT_CLASSIFIED.fetch_add(1, Ordering::Relaxed); + let listing = match entry.listing() { + Ok(listing) => listing, + Err(error) => { +@@ -1855,4 +1860,49 @@ mod tests { + ); + } + } ++ #[test] ++ #[ignore = "manual normal-scan complexity audit on real temporary fixtures"] ++ fn additional_scan_complexity_audit() { ++ for nested in [false, true] { ++ for count in [128, 1024] { ++ let temp = tempfile::tempdir().expect("tempdir"); ++ for index in 0..count { ++ let name = format!("entry-{index:05}"); ++ let file = if nested { ++ let folder = temp.path().join(name); ++ fs::create_dir(&folder).expect("mkdir"); ++ folder.join("payload") ++ } else { ++ temp.path().join(name) ++ }; ++ fs::write(file, [b'x'; 64]).expect("write"); ++ } ++ AUDIT_CLASSIFIED.store(0, Ordering::Relaxed); ++ crate::tree::AUDIT_AGGREGATE.store(0, Ordering::Relaxed); ++ let start = std::time::Instant::now(); ++ let result = scan( ++ temp.path(), ++ ScanOptions { ++ apparent_size: true, ++ threads: crate::scan_threads::ScanThreads { ++ max_threads: 1, ++ ..crate::scan_threads::ScanThreads::default() ++ }, ++ ..ScanOptions::default() ++ }, ++ ) ++ .expect("scan"); ++ assert_eq!(result.files, count as u64); ++ let entries = AUDIT_CLASSIFIED.load(Ordering::Relaxed); ++ let aggregated = ++ crate::tree::AUDIT_AGGREGATE.load(Ordering::Relaxed); ++ assert_eq!(entries, count * if nested { 2 } else { 1 }); ++ assert_eq!(aggregated, entries + 1); ++ eprintln!( ++ "audit real-scan nested={nested} F={count} listed={entries} aggregate-visits={aggregated} wall={:?}", ++ start.elapsed() ++ ); ++ } ++ } ++ } + } +diff --git a/crates/disktree-core/src/space.rs b/crates/disktree-core/src/space.rs +index 8a2bc4e..5e2f8af 100644 +--- a/crates/disktree-core/src/space.rs ++++ b/crates/disktree-core/src/space.rs +@@ -7,6 +7,11 @@ + use std::io; + use std::path::{Path, PathBuf}; + ++#[cfg(test)] ++thread_local! { ++ static AUDIT_MOUNT_PAIRS: std::cell::Cell = const { std::cell::Cell::new(0) }; ++} ++ + /// A volume's capacity in bytes. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct SpaceInfo { +@@ -515,6 +520,8 @@ fn repeated_mounts_excluding( + let mut repeated: Vec = Vec::new(); + for mount in visible { + let elsewhere = kept.iter().find_map(|wider| { ++ #[cfg(test)] ++ AUDIT_MOUNT_PAIRS.set(AUDIT_MOUNT_PAIRS.get() + 1); + if !same_filesystem(wider, mount) { + return None; + } +@@ -1327,4 +1334,28 @@ map auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse) + let space = SpaceInfo::default(); + assert!((space.used_fraction() - 0.0).abs() < f32::EPSILON); + } ++ #[test] ++ #[ignore = "manual complexity audit on synthetic mount metadata"] ++ fn additional_mount_complexity_audit() { ++ for count in [128, 1024] { ++ let mounts: Vec<_> = (0..count) ++ .map(|index| MountView { ++ device: format!("8:{index}"), ++ source: format!("/dev/audit-{index}"), ++ fstype: "ext4".into(), ++ point: PathBuf::from(format!("/audit/mount-{index:05}")), ++ fs_root: PathBuf::from("/"), ++ }) ++ .collect(); ++ AUDIT_MOUNT_PAIRS.set(0); ++ let start = std::time::Instant::now(); ++ let duplicates = repeated_mounts(&mounts, Path::new("/")); ++ assert!(duplicates.is_empty()); ++ eprintln!( ++ "audit mount-startup K={count} pairs={} wall={:?}", ++ AUDIT_MOUNT_PAIRS.get(), ++ start.elapsed() ++ ); ++ } ++ } + } +diff --git a/crates/disktree-core/src/tree.rs b/crates/disktree-core/src/tree.rs +index 6991a86..8724560 100644 +--- a/crates/disktree-core/src/tree.rs ++++ b/crates/disktree-core/src/tree.rs +@@ -10,6 +10,10 @@ use rustc_hash::FxHashSet; + + use crate::classify::{Category, Reclaim}; + ++#[cfg(test)] ++pub(crate) static AUDIT_AGGREGATE: std::sync::atomic::AtomicUsize = ++ std::sync::atomic::AtomicUsize::new(0); ++ + /// What a node represents on disk. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum NodeKind { +@@ -352,6 +356,8 @@ fn aggregate_at( + depth: usize, + seen: Option<&Seen>, + ) { ++ #[cfg(test)] ++ AUDIT_AGGREGATE.fetch_add(1, Ordering::Relaxed); + if !node.is_dir() { + // Every name of a file has the file's size, so one that weighs + // nothing need not be remembered to be charged once. +diff --git a/crates/disktree-core/src/treemap.rs b/crates/disktree-core/src/treemap.rs +index 598b559..ce2fc50 100644 +--- a/crates/disktree-core/src/treemap.rs ++++ b/crates/disktree-core/src/treemap.rs +@@ -13,6 +13,11 @@ + use crate::filter::{Keep, Matches}; + use crate::tree::{Metric, Node}; + ++#[cfg(test)] ++thread_local! { ++ static AUDIT_RATIO: std::cell::Cell = const { std::cell::Cell::new(0) }; ++} ++ + /// An axis-aligned rectangle in viewport pixels. + #[derive(Clone, Copy, Debug, Default, PartialEq)] + pub struct Rect { +@@ -401,6 +406,8 @@ fn worst_ratio(areas: &[f64], row_sum: f64, side: f64) -> f64 { + } + let thickness = row_sum / side; + areas.iter().fold(0.0_f64, |worst, area| { ++ #[cfg(test)] ++ AUDIT_RATIO.set(AUDIT_RATIO.get() + 1); + if *area <= 0.0 || thickness <= 0.0 { + return worst; + } +@@ -722,4 +729,43 @@ mod tests { + let ratio = area_of("src") / area_of("unit_test.txt"); + assert!((ratio - 3.0).abs() < 0.05, "ratio {ratio}"); + } ++ #[test] ++ #[ignore = "manual row-complexity audit on synthetic layout"] ++ fn additional_layout_complexity_audit() { ++ for count in [128, 1024] { ++ let values = vec![1.; count]; ++ AUDIT_RATIO.set(0); ++ let start = std::time::Instant::now(); ++ let rects = squarify(&values, Rect::new(0., 0., 1024., 1024.)); ++ assert_eq!(rects.len(), count); ++ eprintln!( ++ "audit uncapped-squarify F={count} ratio-visits={} wall={:?}", ++ AUDIT_RATIO.get(), ++ start.elapsed() ++ ); ++ let mut root = Node::directory("root"); ++ for index in 0..count { ++ root.children.push(Node::entry( ++ format!("file-{index}"), ++ crate::tree::NodeKind::File, ++ 1, ++ )); ++ } ++ AUDIT_RATIO.set(0); ++ let start = std::time::Instant::now(); ++ let tiles = layout( ++ &root, ++ &[], ++ Rect::new(0., 0., 1024., 1024.), ++ Metric::Bytes, ++ &LayoutOptions::default(), ++ ); ++ assert!(!tiles.is_empty()); ++ eprintln!( ++ "audit capped-layout F={count} ratio-visits={} wall={:?}", ++ AUDIT_RATIO.get(), ++ start.elapsed() ++ ); ++ } ++ } + } diff --git a/docs/performance/mark-resolution/expanded/README.md b/docs/performance/mark-resolution/expanded/README.md new file mode 100644 index 00000000..c4493365 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/README.md @@ -0,0 +1,229 @@ +# Expanded PR #9: construction, absorption and Review status + +This is the final evidence for the three queue issues discovered after the +original PR #9 fixes. The scope is queue construction, marking a parent over +marked descendants, and Review's blocked/covered row lookups. The existing +removal nesting, mark-refresh and cached-crumb fixes remain included. + +A mark is one independently queued file or folder. Marking a folder counts +once regardless of its descendant file count. Long queues are relevant to +programmatic/MCP control; no usage evidence establishes 1,024 independent +marks as a frequent manual workload. This PR does not add an MCP endpoint. + +## Trigger and denominator + +| Operation | What is timed | When the cost is paid | +| --- | --- | --- | +| Queue construction | All 1,024 state-level additions, before any drawing | Building a queue of disjoint paths; files and folders have separate budgets | +| Parent absorption | One call marking a parent over 1,024 child marks, plus an unrelated mark | That marking action, before rendering | +| Blocked/covered Review | Four forced draws, eight observed renders | Repeated Review rendering, including opening/updating the screen | +| Space, hover, C input | One input plus harness rendering/settling | Ordinary interaction with an already populated queue | + +The earlier 6.60 s number was four forced Explore draws/eight prepares on +main, not a single click, one scan, or the time to select all 1,024 paths. +Neither queue construction nor parent absorption here includes painting. +The Review wall measurements below do include row layout/painting and fresh +plan evaluation. Comparisons per render are reported separately from total +wall time. Timed input tests assert zero `Marks::refresh` calls: no rescan is +included in those numbers. + +## Red before green + +The development history was consolidated into one upstream contribution. +Commit IDs below identify the original measured snapshots, archived in +[fork PR #9](https://github.com/huntharo/disktree/pull/9); they are not +separate commits in the final branch. To reproduce a historical stage, +fetch its full SHA from `https://github.com/huntharo/disktree.git` before +checking it out. For example: + +```sh +git fetch https://github.com/huntharo/disktree.git \ + 57d4efde372e7f08e0a2b91a906ff3b870abaddc +git switch --detach FETCH_HEAD +``` + +At `57d4efde372e7f08e0a2b91a906ff3b870abaddc`, the new probes and budgets run +against the original queue/Review algorithms, with the earlier PR #9 fixes +already present. Four regular budgets fail: queue construction, parent +absorption, blocked Review and covered Review. See [red.log](red.log). +The added folder-only variant was also replayed against that unchanged +algorithm and fails; see [red-folders.log](red-folders.log). + +The original development commits were: + +- `e88b9e1`: ordered mark index, indexed ancestor lookup and one-pass + descendant absorption. +- `b71718f`: Review row indexes borrowed from each newly computed plan. +- `1c36fae`: the separate directory-insertion regression budget; it changes + test coverage, not runtime behavior. +- `d886865`: compatibility coverage for raw paths with redundant separators, + preserving the original shortest-stored-spelling ancestor choice. This + does not affect the canonical paths in the timing fixtures. + +The same regular budgets pass after the fixes. They grow input from 128 to +1,024 and require <=16x logical work, plus absolute caps. Queue mutation +allows <=32 units per mark; row status allows <=8 units per queued item per +observed render. Both Review variants also run at 8,192 entries and verify +that exactly 1,200 rows are built per render. + +| Measured path | Before, 128 / 1,024 | Combined fix, 128 / 1,024 | +| --- | ---: | ---: | +| Queue construction, all additions | 16,384 / 1,048,576 | 1,152 / 9,216 | +| Folder queue construction, all additions | 16,384 / 1,048,576 | 1,152 / 9,216 | +| Parent absorption, one action | 8,771 / 528,899 | 266 / 2,058 | +| Blocked Review status, per render | 8,256 / 524,800 | 384 / 3,072 | +| Covered Review status, per render | 8,384 / 525,824 | 386 / 3,074 | + +The blocked fixture queues paths outside the scanned root. The covered +fixture supplies a raw overlapping parent/children queue; normal manual +marking would absorb those overlaps. Both exercise status decoration through +planning and rendering only, with no removal attempt. + +At 8,192 items, both Review cases use 10,592 status units per render, while +keeping the 1,200-row cap. See [green.log](green.log). The tests drive +`Disktree::toggle_mark` or actual GPUI window draws, not an isolated index +helper. They verify surviving mark order, counts, directory/file kinds, +covered/blocked decorations, render counts and the row cap. + +The logical mutation units count mark visits before the fix; after the fix +they count ancestor/range requests, removals and compaction visits. They do +not count internal B-tree comparisons. The complexity guarantee below +includes that logarithmic factor. Review units count status construction and +row lookups after the fix, versus repeated comparison scans before it. These +are deterministic work budgets, not CPU instruction counts. + +## Three-run wall measurements + +Medians in milliseconds from three serial repetitions, macOS arm64, Rust +1.97.1, the repository test profile, `--test-threads=1`. Compilation and +fixture setup are excluded. These workloads use synthetic displayed trees +backed by empty private temporary roots; they establish scaling, not storage +throughput. Raw samples: [wall-before.log](wall-before.log) and +[wall-after.log](wall-after.log). + +| Workload at 1,024 marks | Before expansion | Combined fix | +| --- | ---: | ---: | +| Queue all 1,024 files, no drawing | 176.13 ms | 6.41 ms | +| One parent-absorption action | 15.60 ms | 0.47 ms | +| Blocked Review, four draws/eight renders | 1,003.88 ms | 883.69 ms | +| Covered Review, four draws/eight renders | 1,110.66 ms | 1,009.63 ms | + +Review's wall improvement is much smaller than its row-lookup work reduction: +the screen still lays out up to 1,200 rows and computes plans/guards. +Eliminating one quadratic join does not eliminate those linear and +O(M log M) costs. The larger 8,192-item capped cases took approximately +1.19 s blocked / 2.11 s covered for four draws in these instrumented runs. +No elapsed-time threshold or frame-rate claim is made. + +## Implementation and guarantees + +Let M be current marks, D path depth, K absorbed descendants, B blocked +entries, C covered entries and L displayed rows (L <= 1,200). Path lengths +add comparison/hashing/copying costs to the bounds below. + +`Marks` keeps its ordered `Vec` for display and replaces the private +exact-path hash set with a `BTreeSet`. Native `Path` ordering is by +components, so a strict descendant, if any, immediately follows a parent's +position in that ordered index. Prefix lookalikes such as `a-b`, `a.b` and +`ab` do not defeat that query. + +- Ancestor lookup visits path ancestors and queries the index, costing + O(D log M) index work rather than scanning M marks. +- A disjoint insertion proves that no descendants exist using an ordered + range query. Building M disjoint marks therefore costs O(M*D*log M), + instead of O(M^2). Both files and directories follow this path. +- A parent with K marked descendants compacts the item vector once and + removes those K keys: O(M + K log M), instead of K full compactions. +- Exact mark membership changes from expected hash O(1) to O(log M). The + index still stores one path per mark and adds no fields to scan nodes. +- Unmarking one item still compacts the ordered vector in O(M). Multiple + separately invoked parent actions can each require their own compaction; + this is a per-action bound, not a promise that arbitrary sequences of + scalar edits cost one pass in total. + +`Disktree::toggle_mark` and the existing public behavior stay intact: it +refuses a mark beneath an already marked ancestor, absorbs descendants, +keeps unrelated marks in their original order and reports the absorbed count. +Tree-to-crumb display caches are invalidated by the mutations, including the +new absorption operation. Refresh continues retaining missing paths at zero +bytes, and the ordered index stays consistent with those retained paths. + +`ReviewStatus` borrows covered paths and blocked reasons from the `Plan` +computed for the current render. Construction plus all row lookups costs +expected O(B + C + L) hashing work and O(B + C) temporary index storage. +The first blocked reason wins for duplicate paths, preserving the original +linear lookup's behavior. Item order, chips and the display cap remain. +No plan/status authorization is retained across renders. + +Tests preserve native path component/case semantics, exact non-UTF-8 paths, +first-reason behavior, parent mark retention, unrelated neighbours and cache +invalidation after absorption. Removal guards, measurement and execution +logic are unchanged; worker-side mount/link revalidation still occurs +immediately before deletion. Existing synthetic permanent-deletion tests +continue passing. + +## End-to-end input evidence + +The separate opt-in input benchmark uses real on-disk temporary directories: +1,025 sibling folders, one 64-byte file each, with the same 1,400x900 window +and both panels visible. Space queues one additional folder; hover and C +then run with that additional mark present. Three repetitions record the +actual render/prepare/plan counts and zero refreshes. These inputs cause two +or three renders in the harness, not universally one production frame. + +[interactions-main.log](interactions-main.log) replays main algorithms +(`5a3c92e`, tests/probes over main `6c8d4ce`) with the input test applied. +[interactions-final.log](interactions-final.log) runs the combined fixes +immediately afterward. [interactions-before.log](interactions-before.log) +is a separate control with only the original PR #9 fixes, before this +expansion. A generated table of the latest input medians is kept below. +These test-profile samples are observations, not release-app per-click +guarantees; small queues do not support a substantial speedup claim. + +| Already queued | Space: main → final | Hover: main → final | Review C: main → final | +| ---: | ---: | ---: | ---: | +| 0 | 3.52 → 3.52 ms | 3.64 → 3.58 ms | 3.10 → 3.07 ms | +| 1 | 3.79 → 3.76 ms | 3.94 → 3.87 ms | 3.47 → 3.43 ms | +| 8 | 5.14 → 5.01 ms | 5.28 → 5.14 ms | 5.82 → 5.74 ms | +| 32 | 8.70 → 7.23 ms | 8.85 → 7.43 ms | 13.53 → 12.19 ms | +| 128 | 41.54 → 16.13 ms | 40.85 → 16.26 ms | 62.99 → 38.19 ms | +| 1,024 | 1584.73 → 96.49 ms | 1575.83 → 97.78 ms | 1786.18 → 300.71 ms | + +## Validation and reproduction + +- `cargo xtask lint`: pass; [lint.log](lint.log). +- `cargo xtask test`: 88 app + 150 core + 3 xtask tests pass (241 total); + [tests.log](tests.log). Two pre-existing ignored core tests and the manual + timing test remain ignored by normal CI. +- `cargo test --workspace budget -- --nocapture --test-threads=1`: all + original and expanded budgets pass, including 8,192-entry Review cases. +- The opt-in input timing test passes when explicitly run. +- Local validation is macOS arm64. Other operating systems remain for the + repository's existing CI; no new cross-platform result is claimed. + +The first four red failures can be reproduced at `57d4efd`: + +```sh +cargo test -p disktree-app budget -- --nocapture --test-threads=1 +``` + +At the final head: + +```sh +cargo xtask lint +cargo xtask test +cargo test --workspace budget -- --nocapture --test-threads=1 +cargo test -p disktree-app marked_interaction_timings -- \ + --ignored --nocapture --test-threads=1 +``` + +These changes improve the existing state/render paths. Future programmatic +bulk control should still submit additions within one UI transaction rather +than require a redraw per tool call. Per-render plans continue processing +the queued items, and independently rendering R times still pays that cost +R times. No MCP transport/control implementation is included here. + +Deep breadcrumbs, Linux mount-topology checks and the unusual classification +case from the [broader audit](../additional-audit.md) remain separate +follow-ups. This PR is one main-based review unit for large removal queues; +it does not depend on the APFS feature branch. diff --git a/docs/performance/mark-resolution/expanded/green-marking.log b/docs/performance/mark-resolution/expanded/green-marking.log new file mode 100644 index 00000000..58d28114 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/green-marking.log @@ -0,0 +1,54 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 7.25s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::queue_construction_budget ... queue-construction M=128 work=1152 wall=572.625µs +queue-construction M=1024 work=9216 wall=6.670083ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 85 filtered out; finished in 0.13s + + Finished `test` profile [optimized + debuginfo] target(s) in 0.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::parent_absorption_budget ... parent-absorption M=128 work=266 compactions=130 wall=58.625µs +parent-absorption M=1024 work=2058 compactions=1026 wall=459.125µs +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 85 filtered out; finished in 0.07s + + Finished `test` profile [optimized + debuginfo] target(s) in 0.27s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 13 tests +test marks::tests::display_path_shortens_the_home_prefix ... ok +test marks::tests::ordered_marks_keep_exact_non_utf8_paths ... ok +test marks::tests::ordered_marks_preserve_components_case_and_outermost_ancestor ... ok +test marks::tests::marking_the_same_path_twice_keeps_one_entry ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=23.041µs +ok +test marks::tests::resolver_preserves_lossy_non_utf8_components ... ok +test marks::tests::toggling_marks_and_unmarks ... ok +test marks::tests::find_matches_whole_components_only ... ok +test marks::tests::resolver_preserves_scan_name_semantics ... ok +test marks::tests::refresh_re_reads_sizes_from_a_new_tree ... ok +test marks::tests::absorption_invalidates_cached_crumbs_and_keeps_missing_paths_indexed ... ok +test marks::tests::cached_crumbs_follow_every_invalidation_boundary ... ok +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=50.417µs +refresh M=128 N=256 R=4 work=2560 wall=232.083µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=58.625µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=27.083µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=60.416µs +test marks::tests::refresh_dimension_budget ... ok +refresh M=1024 N=2048 R=4 work=20480 wall=1.545542ms +test marks::tests::refresh_resolution_budget ... ok + +test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.01s + diff --git a/docs/performance/mark-resolution/expanded/green-review.log b/docs/performance/mark-resolution/expanded/green-review.log new file mode 100644 index 00000000..b9d59fa2 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/green-review.log @@ -0,0 +1,29 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 4.06s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 2 tests +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=3072 renders=8 rows=128 per-render=384 wall=117.141208ms +review-status covered=false M=1024 work=24576 renders=8 rows=1024 per-render=3072 wall=918.023375ms +review-status covered=false M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=1.251237917s +ok +test tests::covered_review_status_budget ... review-status covered=true M=128 work=3088 renders=8 rows=129 per-render=386 wall=124.676334ms +review-status covered=true M=1024 work=24592 renders=8 rows=1025 per-render=3074 wall=1.058290834s +review-status covered=true M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=2.1404995s +ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 86 filtered out; finished in 5.87s + + Finished `test` profile [optimized + debuginfo] target(s) in 0.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 2 tests +test views::tests::review_status_preserves_exact_paths_and_first_blocked_reason ... ok +test views::tests::review_status_keeps_non_utf8_paths_distinct ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 86 filtered out; finished in 0.00s + diff --git a/docs/performance/mark-resolution/expanded/green.log b/docs/performance/mark-resolution/expanded/green.log new file mode 100644 index 00000000..62924d9e --- /dev/null +++ b/docs/performance/mark-resolution/expanded/green.log @@ -0,0 +1,57 @@ + Finished `test` profile [optimized + debuginfo] target(s) in 0.35s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 10 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=24.375µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=48.083µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=52.167µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=26.25µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=92.083µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=203.5µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.533958ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=3072 renders=8 rows=128 per-render=384 wall=107.112541ms +review-status covered=false M=1024 work=24576 renders=8 rows=1024 per-render=3072 wall=896.730708ms +review-status covered=false M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=1.196129834s +ok +test tests::covered_review_status_budget ... review-status covered=true M=128 work=3088 renders=8 rows=129 per-render=386 wall=122.642875ms +review-status covered=true M=1024 work=24592 renders=8 rows=1025 per-render=3074 wall=1.006027084s +review-status covered=true M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=2.109495042s +ok +test tests::folder_queue_construction_budget ... folder-queue M=128 work=1152 wall=542µs +folder-queue M=1024 work=9216 wall=6.55425ms +ok +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=60.956167ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=61.972375ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=332.587667ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=333.501ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=266 compactions=130 wall=57.667µs +parent-absorption M=1024 work=2058 compactions=1026 wall=472.542µs +ok +test tests::queue_construction_budget ... queue-construction M=128 work=1152 wall=554.417µs +queue-construction M=1024 work=9216 wall=6.531ms +ok + +test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 79 filtered out; finished in 6.89s + + Running unittests src/lib.rs (target/debug/deps/disktree_core-9300e5ba20d6b25f) + +running 1 test +test removal::tests::removal_nesting_budget ... plan M=128 work=1314 wall=2.843041ms +plan M=1024 work=15298 wall=22.107625ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 151 filtered out; finished in 0.03s + + Running unittests src/main.rs (target/debug/deps/xtask-0dc2df263f51d89e) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s + diff --git a/docs/performance/mark-resolution/expanded/interactions-before.log b/docs/performance/mark-resolution/expanded/interactions-before.log new file mode 100644 index 00000000..93d390f5 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/interactions-before.log @@ -0,0 +1,65 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 6.06s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::marked_interaction_timings ... interaction action=mark-space repeat=1 marks=0->1 renders=3 prepares=3 plans=6 refreshes=0 wall=5.29ms +interaction action=hover repeat=1 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.693417ms +interaction action=review-c repeat=1 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.161125ms +interaction action=mark-space repeat=2 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.493542ms +interaction action=hover repeat=2 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=4.177458ms +interaction action=review-c repeat=2 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.333041ms +interaction action=mark-space repeat=3 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.531208ms +interaction action=hover repeat=3 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.598083ms +interaction action=review-c repeat=3 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.059417ms +interaction action=mark-space repeat=1 marks=1->2 renders=3 prepares=3 plans=6 refreshes=0 wall=6.413958ms +interaction action=hover repeat=1 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=4.211334ms +interaction action=review-c repeat=1 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.781ms +interaction action=mark-space repeat=2 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=4.085666ms +interaction action=hover repeat=2 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=4.282084ms +interaction action=review-c repeat=2 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.605958ms +interaction action=mark-space repeat=3 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=4.143666ms +interaction action=hover repeat=3 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=4.118042ms +interaction action=review-c repeat=3 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.837666ms +interaction action=mark-space repeat=1 marks=8->9 renders=3 prepares=3 plans=6 refreshes=0 wall=8.128709ms +interaction action=hover repeat=1 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.588583ms +interaction action=review-c repeat=1 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=6.106292ms +interaction action=mark-space repeat=2 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.294584ms +interaction action=hover repeat=2 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.593625ms +interaction action=review-c repeat=2 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=6.023584ms +interaction action=mark-space repeat=3 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.320958ms +interaction action=hover repeat=3 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.340792ms +interaction action=review-c repeat=3 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=5.888875ms +interaction action=mark-space repeat=1 marks=32->33 renders=3 prepares=3 plans=6 refreshes=0 wall=11.475584ms +interaction action=hover repeat=1 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.573458ms +interaction action=review-c repeat=1 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=13.598084ms +interaction action=mark-space repeat=2 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.43825ms +interaction action=hover repeat=2 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.692292ms +interaction action=review-c repeat=2 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=13.951083ms +interaction action=mark-space repeat=3 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.248416ms +interaction action=hover repeat=3 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=9.179667ms +interaction action=review-c repeat=3 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=14.361125ms +interaction action=mark-space repeat=1 marks=128->129 renders=3 prepares=3 plans=6 refreshes=0 wall=25.540333ms +interaction action=hover repeat=1 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=17.300917ms +interaction action=review-c repeat=1 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=41.825167ms +interaction action=mark-space repeat=2 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=17.227209ms +interaction action=hover repeat=2 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=17.180542ms +interaction action=review-c repeat=2 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=39.642416ms +interaction action=mark-space repeat=3 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=17.732042ms +interaction action=hover repeat=3 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=18.09625ms +interaction action=review-c repeat=3 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=45.706083ms +interaction action=mark-space repeat=1 marks=1024->1025 renders=3 prepares=3 plans=6 refreshes=0 wall=154.267542ms +interaction action=hover repeat=1 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=106.91175ms +interaction action=review-c repeat=1 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=322.193416ms +interaction action=mark-space repeat=2 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=97.879042ms +interaction action=hover repeat=2 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=99.106959ms +interaction action=review-c repeat=2 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=322.52575ms +interaction action=mark-space repeat=3 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=106.434417ms +interaction action=hover repeat=3 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=104.738875ms +interaction action=review-c repeat=3 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=320.722334ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 82 filtered out; finished in 4.32s + diff --git a/docs/performance/mark-resolution/expanded/interactions-final.log b/docs/performance/mark-resolution/expanded/interactions-final.log new file mode 100644 index 00000000..209a385f --- /dev/null +++ b/docs/performance/mark-resolution/expanded/interactions-final.log @@ -0,0 +1,66 @@ + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 4.71s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::marked_interaction_timings ... interaction action=mark-space repeat=1 marks=0->1 renders=3 prepares=3 plans=6 refreshes=0 wall=5.246292ms +interaction action=hover repeat=1 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.710917ms +interaction action=review-c repeat=1 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.242042ms +interaction action=mark-space repeat=2 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.513208ms +interaction action=hover repeat=2 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.584084ms +interaction action=review-c repeat=2 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.063ms +interaction action=mark-space repeat=3 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.520209ms +interaction action=hover repeat=3 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.578375ms +interaction action=review-c repeat=3 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.073ms +interaction action=mark-space repeat=1 marks=1->2 renders=3 prepares=3 plans=6 refreshes=0 wall=5.520041ms +interaction action=hover repeat=1 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.890709ms +interaction action=review-c repeat=1 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.48725ms +interaction action=mark-space repeat=2 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.755416ms +interaction action=hover repeat=2 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.86925ms +interaction action=review-c repeat=2 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.427584ms +interaction action=mark-space repeat=3 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.751084ms +interaction action=hover repeat=3 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.86175ms +interaction action=review-c repeat=3 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.408333ms +interaction action=mark-space repeat=1 marks=8->9 renders=3 prepares=3 plans=6 refreshes=0 wall=7.470209ms +interaction action=hover repeat=1 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.143125ms +interaction action=review-c repeat=1 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=5.73275ms +interaction action=mark-space repeat=2 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.014375ms +interaction action=hover repeat=2 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.164583ms +interaction action=review-c repeat=2 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=5.76775ms +interaction action=mark-space repeat=3 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.006375ms +interaction action=hover repeat=3 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.13825ms +interaction action=review-c repeat=3 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=5.735209ms +interaction action=mark-space repeat=1 marks=32->33 renders=3 prepares=3 plans=6 refreshes=0 wall=11.012916ms +interaction action=hover repeat=1 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=7.431459ms +interaction action=review-c repeat=1 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=13.125583ms +interaction action=mark-space repeat=2 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=7.209709ms +interaction action=hover repeat=2 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=7.418334ms +interaction action=review-c repeat=2 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=12.185875ms +interaction action=mark-space repeat=3 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=7.22675ms +interaction action=hover repeat=3 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=7.432875ms +interaction action=review-c repeat=3 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=12.091458ms +interaction action=mark-space repeat=1 marks=128->129 renders=3 prepares=3 plans=6 refreshes=0 wall=24.179958ms +interaction action=hover repeat=1 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=16.337667ms +interaction action=review-c repeat=1 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=39.234959ms +interaction action=mark-space repeat=2 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=16.130417ms +interaction action=hover repeat=2 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=16.262167ms +interaction action=review-c repeat=2 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=37.713833ms +interaction action=mark-space repeat=3 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=16.075125ms +interaction action=hover repeat=3 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=16.138417ms +interaction action=review-c repeat=3 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=38.188041ms +interaction action=mark-space repeat=1 marks=1024->1025 renders=3 prepares=3 plans=6 refreshes=0 wall=143.247792ms +interaction action=hover repeat=1 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=99.35325ms +interaction action=review-c repeat=1 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=302.670959ms +interaction action=mark-space repeat=2 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=96.486625ms +interaction action=hover repeat=2 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=95.861042ms +interaction action=review-c repeat=2 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=300.70975ms +interaction action=mark-space repeat=3 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=95.268667ms +interaction action=hover repeat=3 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=97.78325ms +interaction action=review-c repeat=3 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=298.794208ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 88 filtered out; finished in 4.04s + diff --git a/docs/performance/mark-resolution/expanded/interactions-main.log b/docs/performance/mark-resolution/expanded/interactions-main.log new file mode 100644 index 00000000..800e1841 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/interactions-main.log @@ -0,0 +1,66 @@ + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 4.93s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::marked_interaction_timings ... interaction action=mark-space repeat=1 marks=0->1 renders=3 prepares=3 plans=6 refreshes=0 wall=5.255584ms +interaction action=hover repeat=1 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.762417ms +interaction action=review-c repeat=1 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.218458ms +interaction action=mark-space repeat=2 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.503375ms +interaction action=hover repeat=2 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.638375ms +interaction action=review-c repeat=2 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.096042ms +interaction action=mark-space repeat=3 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.517333ms +interaction action=hover repeat=3 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=3.6145ms +interaction action=review-c repeat=3 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=3.080083ms +interaction action=mark-space repeat=1 marks=1->2 renders=3 prepares=3 plans=6 refreshes=0 wall=5.511084ms +interaction action=hover repeat=1 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.941834ms +interaction action=review-c repeat=1 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.503667ms +interaction action=mark-space repeat=2 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.788ms +interaction action=hover repeat=2 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.90975ms +interaction action=review-c repeat=2 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.466166ms +interaction action=mark-space repeat=3 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.76825ms +interaction action=hover repeat=3 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.96025ms +interaction action=review-c repeat=3 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=3.458125ms +interaction action=mark-space repeat=1 marks=8->9 renders=3 prepares=3 plans=6 refreshes=0 wall=7.608625ms +interaction action=hover repeat=1 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.284208ms +interaction action=review-c repeat=1 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=5.894708ms +interaction action=mark-space repeat=2 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.141833ms +interaction action=hover repeat=2 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.286125ms +interaction action=review-c repeat=2 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=5.815959ms +interaction action=mark-space repeat=3 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.117792ms +interaction action=hover repeat=3 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=5.267ms +interaction action=review-c repeat=3 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=5.815375ms +interaction action=mark-space repeat=1 marks=32->33 renders=3 prepares=3 plans=6 refreshes=0 wall=12.998667ms +interaction action=hover repeat=1 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.816834ms +interaction action=review-c repeat=1 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=13.860541ms +interaction action=mark-space repeat=2 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.695333ms +interaction action=hover repeat=2 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.8455ms +interaction action=review-c repeat=2 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=13.520791ms +interaction action=mark-space repeat=3 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.686166ms +interaction action=hover repeat=3 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=8.850291ms +interaction action=review-c repeat=3 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=13.525542ms +interaction action=mark-space repeat=1 marks=128->129 renders=3 prepares=3 plans=6 refreshes=0 wall=58.431709ms +interaction action=hover repeat=1 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=39.571ms +interaction action=review-c repeat=1 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=62.991042ms +interaction action=mark-space repeat=2 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=41.537667ms +interaction action=hover repeat=2 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=42.52325ms +interaction action=review-c repeat=2 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=63.378542ms +interaction action=mark-space repeat=3 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=40.624666ms +interaction action=hover repeat=3 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=40.849792ms +interaction action=review-c repeat=3 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=62.616375ms +interaction action=mark-space repeat=1 marks=1024->1025 renders=3 prepares=3 plans=6 refreshes=0 wall=2.353868208s +interaction action=hover repeat=1 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.5475255s +interaction action=review-c repeat=1 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=1.786180708s +interaction action=mark-space repeat=2 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.558053083s +interaction action=hover repeat=2 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.588539167s +interaction action=review-c repeat=2 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=1.814801708s +interaction action=mark-space repeat=3 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.584729708s +interaction action=hover repeat=3 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.575834417s +interaction action=review-c repeat=3 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=1.766631333s +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 37.74s + diff --git a/docs/performance/mark-resolution/expanded/lint.log b/docs/performance/mark-resolution/expanded/lint.log new file mode 100644 index 00000000..ee7838ac --- /dev/null +++ b/docs/performance/mark-resolution/expanded/lint.log @@ -0,0 +1,7 @@ + Finished `dev` profile [optimized + debuginfo] target(s) in 0.18s + Running `target/debug/xtask lint` + Checking disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `dev` profile [optimized + debuginfo] target(s) in 2.21s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` diff --git a/docs/performance/mark-resolution/expanded/red-folders.log b/docs/performance/mark-resolution/expanded/red-folders.log new file mode 100644 index 00000000..fd240b02 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/red-folders.log @@ -0,0 +1,23 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 4.21s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::folder_queue_construction_budget ... folder-queue M=128 work=16384 wall=2.888959ms +folder-queue M=1024 work=1048576 wall=177.786542ms + +thread 'tests::folder_queue_construction_budget' (404519213) panicked at crates/disktree-app/src/tests.rs:2365:5: +8x folders: 16384 -> 1048576 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED + +failures: + +failures: + tests::folder_queue_construction_budget + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 83 filtered out; finished in 0.32s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` diff --git a/docs/performance/mark-resolution/expanded/red.log b/docs/performance/mark-resolution/expanded/red.log new file mode 100644 index 00000000..81a829b1 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/red.log @@ -0,0 +1,61 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Finished `test` profile [optimized + debuginfo] target(s) in 7.61s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 9 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=62.75µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=45µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=58.083µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=28.875µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=62.834µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=205.458µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.533583ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=66048 renders=8 rows=128 per-render=8256 wall=113.90025ms +review-status covered=false M=1024 work=4198400 renders=8 rows=1024 per-render=524800 wall=1.038960833s + +thread 'tests::blocked_review_status_budget' (404491031) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 66048/8 -> 4198400/8 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED +test tests::covered_review_status_budget ... review-status covered=true M=128 work=67072 renders=8 rows=129 per-render=8384 wall=135.896292ms +review-status covered=true M=1024 work=4206592 renders=8 rows=1025 per-render=525824 wall=1.29864975s + +thread 'tests::covered_review_status_budget' (404491108) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 67072/8 -> 4206592/8 +FAILED +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=63.308583ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=64.349292ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=342.802833ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=360.040792ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=8771 compactions=8512 wall=347.334µs +parent-absorption M=1024 work=528899 compactions=526848 wall=15.802708ms + +thread 'tests::parent_absorption_budget' (404491437) panicked at crates/disktree-app/src/tests.rs:2223:5: +8x descendants: 8771 -> 528899 +FAILED +test tests::queue_construction_budget ... queue-construction M=128 work=16384 wall=2.851667ms +queue-construction M=1024 work=1048576 wall=183.52175ms + +thread 'tests::queue_construction_budget' (404491448) panicked at crates/disktree-app/src/tests.rs:2181:5: +8x marks: 16384 -> 1048576 +FAILED + +failures: + +failures: + tests::blocked_review_status_budget + tests::covered_review_status_budget + tests::parent_absorption_budget + tests::queue_construction_budget + +test result: FAILED. 5 passed; 4 failed; 0 ignored; 0 measured; 74 filtered out; finished in 4.00s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` diff --git a/docs/performance/mark-resolution/expanded/tests.log b/docs/performance/mark-resolution/expanded/tests.log new file mode 100644 index 00000000..a27e00a6 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/tests.log @@ -0,0 +1,275 @@ + Finished `dev` profile [optimized + debuginfo] target(s) in 0.11s + Running `target/debug/xtask test` + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 3.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 89 tests +test appearance::tests::a_linux_desktop_is_followed_only_if_it_states_a_preference ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::display_path_shortens_the_home_prefix ... ok +test git::tests::summaries_say_what_would_be_lost ... ok +test marks::tests::marking_the_same_path_twice_keeps_one_entry ... ok +test marks::tests::ordered_marks_keep_exact_non_utf8_paths ... ok +test marks::tests::ordered_marks_preserve_components_case_and_outermost_ancestor ... ok +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test marks::tests::resolver_preserves_lossy_non_utf8_components ... ok +test marks::tests::toggling_marks_and_unmarks ... ok +test marks::tests::find_matches_whole_components_only ... ok +test marks::tests::refresh_re_reads_sizes_from_a_new_tree ... ok +test marks::tests::absorption_invalidates_cached_crumbs_and_keeps_missing_paths_indexed ... ok +test marks::tests::cached_crumbs_follow_every_invalidation_boundary ... ok +test marks::tests::resolver_preserves_scan_name_semantics ... ok +test palette::tests::age_buckets_cover_every_age_in_order ... ok +test palette::tests::colourful_categories_share_one_level ... ok +test palette::tests::deeper_tiles_lift_away_from_the_background ... ok +test palette::tests::every_legend_category_has_its_own_hue ... ok +test palette::tests::mix_clamps_its_parameter ... ok +test palette::tests::mixing_toward_a_grey_keeps_the_hue ... ok +test palette::tests::the_highlight_is_not_a_category_colour ... ok +test power::tests::presets_are_fixed_and_show_the_effective_cpu_limit ... ok +test power::tests::presets_that_add_no_workers_are_not_offered ... ok +test state::tests::a_transition_grows_a_tile_that_is_being_entered ... ok +test state::tests::a_transition_starts_where_the_region_was_and_ends_where_it_is ... ok +test state::tests::clamping_never_leaves_a_margin ... ok +test state::tests::directions_only_see_gaps_on_their_own_side ... ok +test argument_tests::saved_power_is_loaded_before_cli_overrides ... ok +test state::tests::projecting_and_unprojecting_are_inverses ... ok +test state::tests::zooming_keeps_the_point_under_the_cursor_still ... ok +test marks::tests::refresh_dimension_budget ... ok +test power::tests::saved_choice_survives_replacement_and_bad_data_is_reported ... ok +test marks::tests::refresh_resolution_budget ... ok +test git::tests::reads_a_real_checkout ... ok +test git::tests::a_checkout_cannot_make_git_run_its_programs ... ok +test git::tests::a_checkout_cannot_make_git_verify_a_signature_with_its_program ... ok +test git::tests::a_checkouts_own_filter_is_never_run ... ok +test tests::escape_cancels_widening_and_keeps_the_tree_on_screen ... ok +test tests::escape_in_the_delete_dialog_cancels ... ok +test tests::command_chords_are_not_read_as_plain_letters ... ok +test tests::enter_before_the_search_lands_applies_it_when_it_does ... ok +test tests::marked_interaction_timings ... ignored, manual interaction timing on synthetic temporary files +test tests::a_view_without_flags_or_marks_still_draws ... ok +test tests::a_parents_name_gets_its_own_band_above_its_children ... ok +test tests::a_permanent_deletion_asks_in_an_alert_dialog_then_removes ... ok +test tests::a_crumb_lists_its_siblings_and_jumps_sideways ... ok +test tests::enter_in_the_focused_volume_picker_scans_the_selected_root ... ok +test tests::folder_queue_construction_budget ... ok +test tests::enter_opens_the_selected_directory_at_any_depth ... ok +test tests::restart_arguments_parse_back_to_the_same_scan ... ok +test tests::after_descending_every_tile_is_inside_the_directory_drawn ... ok +test tests::both_appearances_draw ... ok +test tests::back_and_forward_retrace_where_you_have_been ... ok +test tests::dragging_the_panel_edge_resizes_it_within_its_limits ... ok +test tests::escape_cancels_the_first_scan_and_r_starts_it_again ... ok +test tests::hovering_reports_the_tile_under_the_pointer ... ok +test tests::interface_zoom_scales_the_rem_and_the_header_band ... ok +test tests::marked_tiles_follow_metric_rescan_and_mark_changes ... ok +test tests::marking_a_directory_marks_everything_inside_it ... ok +test tests::power_efficiency_menu_saves_without_discarding_the_tree ... ok +test tests::showing_a_tile_that_is_gone_says_so_instead ... ok +test tests::keys_walk_the_tree_and_mark_what_is_selected ... ok +test tests::switching_the_metric_keeps_the_directory_and_the_selection ... ok +test tests::mouse_side_buttons_go_back_and_forward ... ok +test tests::parent_absorption_budget ... ok +test tests::right_click_selects_the_tile_and_reveals_it ... ok +test tests::the_first_scan_shows_what_it_is_doing_then_the_treemap ... ok +test views::tests::review_status_keeps_non_utf8_paths_distinct ... ok +test views::tests::review_status_preserves_exact_paths_and_first_blocked_reason ... ok +test widgets::tests::ages_read_in_the_unit_a_person_would_use ... ok +test widgets::tests::sizes_split_into_number_and_unit ... ok +test tests::the_help_overlay_opens_and_closes ... ok +test tests::the_mark_key_follows_the_pointer_until_the_keyboard_moves ... ok +test tests::queue_construction_budget ... ok +test tests::the_power_menu_offers_only_what_the_cpus_can_tell_apart ... ok +test tests::the_review_screen_copies_the_list_as_an_agent_prompt ... ok +test tests::the_review_screen_switches_removal_mode ... ok +test tests::the_trash_is_the_default_when_there_is_one ... ok +test tests::the_volume_picker_is_centred_and_still_dismisses_from_outside ... ok +test tests::marked_window_resolution_budget ... ok +test tests::the_wheel_goes_into_the_directory_it_zoomed_into ... ok +test tests::the_treemap_zooms_with_the_wheel_and_resets ... ok +test tests::the_volume_picker_opens_moves_and_closes ... ok +test tests::typing_filters_live_and_enter_shows_only_the_matches ... ok +test tests::widening_reuses_the_tree_it_has_and_reads_only_the_rest ... ok +test tests::the_window_draws_a_treemap_with_tiles ... ok +test tests::blocked_review_status_budget ... ok +test tests::covered_review_status_budget ... ok + +test result: ok. 88 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 4.87s + + Running unittests src/lib.rs (target/debug/deps/disktree_core-9300e5ba20d6b25f) + +running 152 tests +test classify::tests::macos_developer_leftovers_are_reclaimable_and_its_risks_are_not ... ok +test export::tests::the_list_is_one_path_per_line ... ok +test classify::tests::steam_libraries_and_emulation_are_media_not_disposable_caches ... ok +test filter::tests::case_is_ignored_without_allocating ... ok +test classify::tests::the_legend_lists_every_named_category_once ... ok +test classify::tests::windows_caches_and_sync_folders_are_recognized ... ok +test export::tests::a_name_with_a_newline_cannot_smuggle_in_a_path ... ok +test export::tests::a_name_that_is_not_unicode_is_flagged_not_listed ... ok +test export::tests::the_prompt_lists_every_path_with_its_size_and_the_rules ... ok +test filter::tests::an_empty_needle_filters_nothing ... ok +test filter::tests::a_search_below_the_root_leaves_the_rest_alone ... ok +test filter::tests::matches_are_kept_whole_and_their_ancestors_by_what_matched ... ok +test access::tests::only_macos_has_an_answer_and_a_missing_database_is_not_a_refusal ... ok +test classify::tests::a_steam_library_on_another_drive_is_media_not_its_temp_folder ... ok +test classify::tests::sandbox_layers_and_snapshots_are_reclaimable_only_in_sandbox_state ... ok +test insights::tests::the_limit_keeps_the_largest ... ok +test insights::tests::ranks_findings_largest_first_and_skips_the_tiny ... ok +test insights::tests::documents_are_never_suggested ... ok +test insights::tests::crumbs_address_the_finding_from_the_root ... ok +test classify::tests::names_announce_their_kind_and_children_inherit_it ... ok +test classify::tests::target_is_build_output_only_beside_a_cargo_manifest ... ok +test classify::tests::an_unknown_top_level_directory_takes_its_largest_known_child ... ok +test classify::tests::caches_are_reclaimable_all_the_way_down ... ok +test removal::tests::a_directory_holding_a_system_tree_is_refused ... ok +test removal::tests::a_directory_holding_the_home_directory_is_refused ... ok +test macos::tests::a_missing_directory_is_an_error ... ok +test removal::tests::macos_always_uses_the_system_trash ... ok +test removal::tests::macos_guard_keys_fold_the_data_volume_and_case ... ok +test removal::tests::macos_guards_see_through_other_spellings ... ok +test removal::tests::macos_system_trees_are_refused_but_apps_are_not ... ok +test removal::tests::a_name_that_is_not_utf8_is_refused_before_the_trash_sees_it ... ok +test removal::tests::names_win32_would_misread_are_recognized ... ok +test macos::tests::a_listing_has_names_kinds_lengths_and_no_dot_entries ... ok +test removal::tests::nesting_keeps_component_boundaries_duplicates_and_output_order ... ok +test removal::tests::normalize_resolves_dots_without_the_filesystem ... ok +test macos::tests::names_are_text_and_open ... ok +test removal::tests::nesting_uses_guard_case_order_even_when_the_child_sorts_first ... ok +test macos::tests::every_leaf_reads_as_lstat_reads_it ... ok +test removal::tests::a_mount_below_a_target_is_caught_again_at_removal_time ... ok +test removal::tests::a_volume_is_judged_by_its_mount_where_the_kernel_names_one ... ok +test removal::tests::percent_encoding_escapes_what_the_spec_requires ... ok +test removal::tests::a_missing_target_reports_not_found ... ok +test removal::tests::a_plan_keeps_only_the_outermost_targets ... ok +test removal::tests::a_target_with_a_mount_inside_is_refused ... ok +test removal::tests::mount_points_are_recognized ... ok +test removal::tests::a_trash_info_path_is_restorable ... ok +test removal::tests::a_target_reached_through_a_link_is_refused ... ok +test removal::tests::paths_outside_the_root_are_refused ... ok +test removal::tests::permanent_removal_takes_directories_and_leaves_siblings ... ok +test removal::tests::a_removal_run_reports_every_item_and_a_total ... ok +test removal::tests::the_macos_trash_takes_a_file_and_leaves_a_linked_target ... ignored, puts a file in this Mac's real Trash +test removal::tests::home_is_found_by_its_real_path_too ... ok +test removal::tests::permanent_removal_follows_links_above_the_root ... ok +test removal::tests::permanent_removal_does_not_follow_a_link_put_on_the_way_down ... ok +test removal::tests::permanent_removal_unlinks_nested_symlinks_without_following_them ... ok +test removal::tests::permanent_removal_takes_nested_trees_and_hidden_files ... ok +test removal::tests::permanent_removal_unlinks_a_symlink_instead_of_following_it ... ok +test removal::tests::nothing_is_mounted_below_a_temp_dir ... ok +test removal::tests::read_only_files_are_removed_too ... ok +test removal::tests::sibling_prefixes_are_not_treated_as_containment ... ok +test removal::tests::a_link_put_on_the_way_down_after_planning_stops_the_worker ... ok +test removal::tests::the_mount_table_is_read_off_the_calling_thread_and_then_kept ... ok +test removal::tests::trash_names_avoid_collisions ... ok +test removal::tests::the_root_and_home_are_refused ... ok +test removal::tests::the_xdg_trash_moves_a_file_and_records_where_it_came_from ... ok +test scan::tests::a_file_root_is_rejected ... ok +test scan::tests::fixed_worker_pools_change_size_without_reinitializing_rayon ... ok +test removal::tests::this_macs_mount_table_is_read_without_proc ... ok +test scan::tests::disk_usage_is_whole_allocation_units ... ok +test scan::tests::followed_symlink_loops_do_not_hang_the_scan ... ok +test scan::tests::hardlinks_are_charged_once_by_default ... ok +test scan::tests::an_unreadable_directory_is_recorded_not_fatal ... ok +test scan::tests::a_spawned_scan_reports_progress_and_a_result ... ok +test scan::tests::a_directory_reports_its_own_bytes_separately ... ok +test scan::tests::whole_disk_smoke ... ignored, walks the whole disk +test scan::tests::children_are_ranked_largest_first ... ok +test scan_threads::tests::governor_waits_for_retirement_and_recovers_without_exceeding_target ... ok +test scan_threads::tests::inconclusive_windows_cannot_accept_a_reduction ... ok +test scan_threads::tests::increased_limit_is_not_supplied_until_jobs_are_dispatched ... ok +test scan_threads::tests::long_directory_never_becomes_a_fully_supplied_window ... ok +test scan_threads::tests::search_refines_one_reference_without_compounding_loss ... ok +test size::tests::a_value_that_rounds_up_to_the_next_unit_is_shown_in_it ... ok +test size::tests::bytes_use_binary_units_with_one_decimal_below_ten ... ok +test size::tests::counts_switch_to_metric_suffixes ... ok +test size::tests::share_bar_is_always_the_requested_width ... ok +test scan_threads::tests::real_pool_retires_at_job_boundaries_and_drains_queued_descendants ... ok +test size::tests::share_survives_a_zero_total ... ok +test size::tests::short_form_drops_the_space ... ok +test space::tests::a_bind_mount_inside_the_scan_is_left_out ... ok +test space::tests::a_home_scan_has_no_foreign_mounts_here ... ok +test space::tests::a_missing_path_reports_the_io_error ... ok +test space::tests::a_real_volume_reports_plausible_numbers ... ok +test space::tests::a_root_booted_from_a_snapshot_keeps_its_subvolumes ... ok +test space::tests::a_table_that_does_not_place_a_path_claims_nothing ... ok +test space::tests::a_volume_includes_its_subvolumes_and_nothing_else ... ok +test space::tests::a_volume_keeps_its_subvolumes_and_loses_everything_else ... ok +test space::tests::btrfs_subvolume_devices_can_differ ... ok +test space::tests::excluded_views_cannot_replace_visible_files ... ok +test space::tests::hidden_original_and_nested_mounts_are_preserved ... ok +test space::tests::macos_mount_points_keep_spaces_and_brackets ... ok +test space::tests::mountinfo_escapes_are_decoded_once ... ok +test space::tests::only_macos_skips_the_data_volume_and_in_the_roots_spelling ... ok +test space::tests::projecting_removal_cannot_exceed_the_volume ... ok +test space::tests::separate_home_disk_is_its_own_volume ... ok +test space::tests::subvolumes_are_not_repeats_but_a_second_mount_of_a_disk_is ... ok +test space::tests::the_device_is_the_longest_matching_mount ... ok +test scan::tests::file_count_metric_ranks_by_entries ... ok +test space::tests::the_home_disk_on_macos_is_the_root_and_has_a_device ... ok +test space::tests::the_top_of_a_btrfs_disk_beside_its_subvolumes_is_counted_once ... ok +test space::tests::the_whole_disk_is_the_top_of_the_home_volume ... ok +test space::tests::unrelated_tmpfs_mounts_are_not_duplicates ... ok +test space::tests::used_fraction_handles_an_empty_volume_report ... ok +test space::tests::volume_candidates_are_real_disks_not_pseudo_filesystems ... ok +test space::tests::volumes_with_least_space_come_first_and_unknowns_last ... ok +test tree::tests::a_node_stays_small ... ok +test tree::tests::aggregate_derives_totals_and_orders_children ... ok +test scan::tests::a_wider_scan_reuses_the_subtree_it_already_knows ... ok +test tree::tests::depth_counts_edges ... ok +test tree::tests::find_reports_crumbs_and_is_case_insensitive ... ok +test tree::tests::aggregate_can_rank_by_file_count ... ok +test tree::tests::identities_that_differ_only_in_their_top_bits_are_two_files ... ok +test tree::tests::largest_child_is_the_first_child_because_children_are_sorted ... ok +test tree::tests::path_of_joins_names_beneath_the_root ... ok +test tree::tests::resolve_walks_child_indices ... ok +test treemap::tests::a_leaf_never_claims_a_header ... ok +test treemap::tests::a_filtered_layout_shows_only_the_matches_at_their_size ... ok +test treemap::tests::a_subdivided_directory_keeps_a_header_for_its_own_name ... ok +test treemap::tests::a_tile_without_room_for_a_header_stays_whole ... ok +test treemap::tests::hit_returns_the_deepest_tile ... ok +test treemap::tests::layout_depth_one_stops_at_direct_children ... ok +test tree::tests::seen_charges_an_identity_once_in_either_store ... ok +test treemap::tests::layout_merges_the_child_tail_into_one_tile ... ok +test treemap::tests::squarify_fills_the_area ... ok +test treemap::tests::layout_nests_children_inside_their_parent ... ok +test space::tests::without_a_table_the_same_place_is_one_volume ... ok +test scan::tests::hidden_entries_are_included_by_default_and_excluded_on_request ... ok +test treemap::tests::squarify_keeps_aspect_ratios_reasonable ... ok +test treemap::tests::squarify_survives_degenerate_input ... ok +test treemap::tests::tile_crumbs_address_the_scanned_tree_not_the_drawn_node ... ok +test scan::tests::max_depth_stops_descending_but_keeps_direct_files ... ok +test scan::tests::symlinks_are_not_followed_by_default ... ok +test scan::tests::totals_are_summed_bottom_up ... ok +test removal::tests::system_trees_are_refused_in_a_whole_disk_scan ... ok +test removal::tests::the_directories_above_home_are_refused ... ok +test removal::tests::removal_nesting_budget ... ok +test scan::tests::cancelled_queued_jobs_release_parent_completion_tokens ... ok +test scan::tests::fixed_worker_pools_preserve_a_wide_deep_tree ... ok +test removal::tests::only_the_scanned_volumes_marks_are_projected ... ok +test removal::tests::a_failing_trash_tool_reports_its_stderr ... ok +test scan_threads::tests::controller_retires_recovers_after_search_and_cancels ... ok +test macos::tests::a_directory_larger_than_the_buffer_is_listed_whole ... ok +test removal::tests::a_trash_tool_is_called_with_the_path_after_a_separator ... ok + +test result: ok. 150 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.28s + + Running unittests src/main.rs (target/debug/deps/xtask-0dc2df263f51d89e) + +running 3 tests +test bundle::tests::signing_is_ad_hoc_unless_an_identity_is_given ... ok +test bundle::tests::the_plist_template_names_the_executable_and_takes_the_version ... ok +test bundle::tests::notarizing_needs_a_real_signature ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests disktree_core + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + diff --git a/docs/performance/mark-resolution/expanded/wall-after.log b/docs/performance/mark-resolution/expanded/wall-after.log new file mode 100644 index 00000000..21a9e7eb --- /dev/null +++ b/docs/performance/mark-resolution/expanded/wall-after.log @@ -0,0 +1,127 @@ +REPEAT 1: sources at combined fix b71718f + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 4.24s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 9 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=144.833µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=62.791µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=74.667µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=38.75µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=87.5µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=295.792µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.906625ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=3072 renders=8 rows=128 per-render=384 wall=107.559125ms +review-status covered=false M=1024 work=24576 renders=8 rows=1024 per-render=3072 wall=883.20225ms +review-status covered=false M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=1.196734875s +ok +test tests::covered_review_status_budget ... review-status covered=true M=128 work=3088 renders=8 rows=129 per-render=386 wall=122.079458ms +review-status covered=true M=1024 work=24592 renders=8 rows=1025 per-render=3074 wall=997.160667ms +review-status covered=true M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=2.106200625s +ok +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=59.356875ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=60.529083ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=331.029917ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=335.875875ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=266 compactions=130 wall=58.125µs +parent-absorption M=1024 work=2058 compactions=1026 wall=467.083µs +ok +test tests::queue_construction_budget ... queue-construction M=128 work=1152 wall=542µs +queue-construction M=1024 work=9216 wall=6.311584ms +ok + +test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 79 filtered out; finished in 6.71s + + +EXIT 0 +REPEAT 2: sources at combined fix b71718f + Finished `test` profile [optimized + debuginfo] target(s) in 0.27s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 9 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=25.25µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=42.666µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=51.75µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=24.5µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=56.75µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=198.292µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.479125ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=3072 renders=8 rows=128 per-render=384 wall=105.373709ms +review-status covered=false M=1024 work=24576 renders=8 rows=1024 per-render=3072 wall=883.687ms +review-status covered=false M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=1.188244s +ok +test tests::covered_review_status_budget ... review-status covered=true M=128 work=3088 renders=8 rows=129 per-render=386 wall=122.092208ms +review-status covered=true M=1024 work=24592 renders=8 rows=1025 per-render=3074 wall=1.010354417s +review-status covered=true M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=2.112680083s +ok +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=61.657666ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=61.290042ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=335.296459ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=333.089792ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=266 compactions=130 wall=57.125µs +parent-absorption M=1024 work=2058 compactions=1026 wall=481.292µs +ok +test tests::queue_construction_budget ... queue-construction M=128 work=1152 wall=547.333µs +queue-construction M=1024 work=9216 wall=6.518083ms +ok + +test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 79 filtered out; finished in 6.70s + + +EXIT 0 +REPEAT 3: sources at combined fix b71718f + Finished `test` profile [optimized + debuginfo] target(s) in 0.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 9 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=25.583µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=42.583µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=51.292µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=26.042µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=59.083µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=214µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.552917ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=3072 renders=8 rows=128 per-render=384 wall=108.225542ms +review-status covered=false M=1024 work=24576 renders=8 rows=1024 per-render=3072 wall=886.057917ms +review-status covered=false M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=1.19339425s +ok +test tests::covered_review_status_budget ... review-status covered=true M=128 work=3088 renders=8 rows=129 per-render=386 wall=122.399375ms +review-status covered=true M=1024 work=24592 renders=8 rows=1025 per-render=3074 wall=1.009629625s +review-status covered=true M=8192 work=84736 renders=8 rows=1200 per-render=10592 wall=2.127712375s +ok +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=61.806167ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=61.474458ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=351.863667ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=336.533542ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=266 compactions=130 wall=57.75µs +parent-absorption M=1024 work=2058 compactions=1026 wall=459.042µs +ok +test tests::queue_construction_budget ... queue-construction M=128 work=1152 wall=547.75µs +queue-construction M=1024 work=9216 wall=6.406375ms +ok + +test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 79 filtered out; finished in 6.76s + + +EXIT 0 diff --git a/docs/performance/mark-resolution/expanded/wall-before.log b/docs/performance/mark-resolution/expanded/wall-before.log new file mode 100644 index 00000000..d2c6de76 --- /dev/null +++ b/docs/performance/mark-resolution/expanded/wall-before.log @@ -0,0 +1,187 @@ +REPEAT 1: sources at red commit 57d4efd + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 6.29s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 9 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=88.833µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=66.209µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=72.458µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=40.208µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=96.25µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=292.5µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.89825ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=66048 renders=8 rows=128 per-render=8256 wall=111.492791ms +review-status covered=false M=1024 work=4198400 renders=8 rows=1024 per-render=524800 wall=1.003875917s + +thread 'tests::blocked_review_status_budget' (404509605) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 66048/8 -> 4198400/8 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED +test tests::covered_review_status_budget ... review-status covered=true M=128 work=67072 renders=8 rows=129 per-render=8384 wall=122.635083ms +review-status covered=true M=1024 work=4206592 renders=8 rows=1025 per-render=525824 wall=1.110662959s + +thread 'tests::covered_review_status_budget' (404509677) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 67072/8 -> 4206592/8 +FAILED +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=60.112708ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=61.191625ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=333.839916ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=332.639625ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=8771 compactions=8512 wall=332.125µs +parent-absorption M=1024 work=528899 compactions=526848 wall=15.169208ms + +thread 'tests::parent_absorption_budget' (404509733) panicked at crates/disktree-app/src/tests.rs:2223:5: +8x descendants: 8771 -> 528899 +FAILED +test tests::queue_construction_budget ... queue-construction M=128 work=16384 wall=2.928667ms +queue-construction M=1024 work=1048576 wall=176.133125ms + +thread 'tests::queue_construction_budget' (404509745) panicked at crates/disktree-app/src/tests.rs:2181:5: +8x marks: 16384 -> 1048576 +FAILED + +failures: + +failures: + tests::blocked_review_status_budget + tests::covered_review_status_budget + tests::parent_absorption_budget + tests::queue_construction_budget + +test result: FAILED. 5 passed; 4 failed; 0 ignored; 0 measured; 74 filtered out; finished in 3.77s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` + +EXIT 101 +REPEAT 2: sources at red commit 57d4efd + Finished `test` profile [optimized + debuginfo] target(s) in 0.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 9 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=19.958µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=43.5µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=49.75µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=29.916µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=59.417µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=206.042µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.547208ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=66048 renders=8 rows=128 per-render=8256 wall=113.726291ms +review-status covered=false M=1024 work=4198400 renders=8 rows=1024 per-render=524800 wall=1.030923916s + +thread 'tests::blocked_review_status_budget' (404509806) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 66048/8 -> 4198400/8 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED +test tests::covered_review_status_budget ... review-status covered=true M=128 work=67072 renders=8 rows=129 per-render=8384 wall=123.684209ms +review-status covered=true M=1024 work=4206592 renders=8 rows=1025 per-render=525824 wall=1.122654292s + +thread 'tests::covered_review_status_budget' (404509980) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 67072/8 -> 4206592/8 +FAILED +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=60.245708ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=61.801583ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=341.867208ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=350.583125ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=8771 compactions=8512 wall=327.959µs +parent-absorption M=1024 work=528899 compactions=526848 wall=15.596208ms + +thread 'tests::parent_absorption_budget' (404510061) panicked at crates/disktree-app/src/tests.rs:2223:5: +8x descendants: 8771 -> 528899 +FAILED +test tests::queue_construction_budget ... queue-construction M=128 work=16384 wall=2.95175ms +queue-construction M=1024 work=1048576 wall=184.469666ms + +thread 'tests::queue_construction_budget' (404510074) panicked at crates/disktree-app/src/tests.rs:2181:5: +8x marks: 16384 -> 1048576 +FAILED + +failures: + +failures: + tests::blocked_review_status_budget + tests::covered_review_status_budget + tests::parent_absorption_budget + tests::queue_construction_budget + +test result: FAILED. 5 passed; 4 failed; 0 ignored; 0 measured; 74 filtered out; finished in 3.81s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` + +EXIT 101 +REPEAT 3: sources at red commit 57d4efd + Finished `test` profile [optimized + debuginfo] target(s) in 0.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 9 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=19.625µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=44.666µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=52.459µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=24.583µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=61.791µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=199.917µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.529833ms +ok +test tests::blocked_review_status_budget ... review-status covered=false M=128 work=66048 renders=8 rows=128 per-render=8256 wall=108.861834ms +review-status covered=false M=1024 work=4198400 renders=8 rows=1024 per-render=524800 wall=999.468209ms + +thread 'tests::blocked_review_status_budget' (404510132) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 66048/8 -> 4198400/8 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED +test tests::covered_review_status_budget ... review-status covered=true M=128 work=67072 renders=8 rows=129 per-render=8384 wall=124.821916ms +review-status covered=true M=1024 work=4206592 renders=8 rows=1025 per-render=525824 wall=1.110591625s + +thread 'tests::covered_review_status_budget' (404510197) panicked at crates/disktree-app/src/tests.rs:2298:5: +8x status rows: 67072/8 -> 4206592/8 +FAILED +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=59.989667ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=61.309958ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=341.035208ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=332.707042ms +ok +test tests::parent_absorption_budget ... parent-absorption M=128 work=8771 compactions=8512 wall=336µs +parent-absorption M=1024 work=528899 compactions=526848 wall=15.915917ms + +thread 'tests::parent_absorption_budget' (404510244) panicked at crates/disktree-app/src/tests.rs:2223:5: +8x descendants: 8771 -> 528899 +FAILED +test tests::queue_construction_budget ... queue-construction M=128 work=16384 wall=2.838333ms +queue-construction M=1024 work=1048576 wall=175.767541ms + +thread 'tests::queue_construction_budget' (404510255) panicked at crates/disktree-app/src/tests.rs:2181:5: +8x marks: 16384 -> 1048576 +FAILED + +failures: + +failures: + tests::blocked_review_status_budget + tests::covered_review_status_budget + tests::parent_absorption_budget + tests::queue_construction_budget + +test result: FAILED. 5 passed; 4 failed; 0 ignored; 0 measured; 74 filtered out; finished in 3.72s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` + +EXIT 101 diff --git a/docs/performance/mark-resolution/green.log b/docs/performance/mark-resolution/green.log new file mode 100644 index 00000000..64b445ee --- /dev/null +++ b/docs/performance/mark-resolution/green.log @@ -0,0 +1,40 @@ + Finished `test` profile [optimized + debuginfo] target(s) in 0.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 5 tests +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +dimensions folders=128 M=8 D=2 R=1 work=160 wall=30.334µs +refresh M=128 N=256 R=4 work=2560 wall=241.75µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=46.917µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=71.5µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=45µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=88.459µs +test marks::tests::refresh_dimension_budget ... ok +refresh M=1024 N=2048 R=4 work=20480 wall=1.580625ms +test marks::tests::refresh_resolution_budget ... ok +warm window M=128 prepares=8 resolution=216 tiles=1544 wall=61.302208ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=61.50175ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=354.14925ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=341.058584ms +test tests::marked_window_resolution_budget ... ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.87s + + Running unittests src/lib.rs (target/debug/deps/disktree_core-9300e5ba20d6b25f) + +running 1 test +plan M=128 work=1314 wall=2.802916ms +plan M=1024 work=15298 wall=21.911541ms +test removal::tests::removal_nesting_budget ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 151 filtered out; finished in 0.03s + + Running unittests src/main.rs (target/debug/deps/xtask-0dc2df263f51d89e) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s + diff --git a/docs/performance/mark-resolution/interactions-green.log b/docs/performance/mark-resolution/interactions-green.log new file mode 100644 index 00000000..a17c207c --- /dev/null +++ b/docs/performance/mark-resolution/interactions-green.log @@ -0,0 +1,65 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 2.78s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::marked_interaction_timings ... interaction action=mark-space repeat=1 marks=0->1 renders=3 prepares=3 plans=6 refreshes=0 wall=3.678416ms +interaction action=hover repeat=1 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.546375ms +interaction action=review-c repeat=1 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=2.183334ms +interaction action=mark-space repeat=2 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.440541ms +interaction action=hover repeat=2 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.752917ms +interaction action=review-c repeat=2 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=2.195ms +interaction action=mark-space repeat=3 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.512208ms +interaction action=hover repeat=3 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.463708ms +interaction action=review-c repeat=3 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=2.125291ms +interaction action=mark-space repeat=1 marks=1->2 renders=3 prepares=3 plans=6 refreshes=0 wall=3.916458ms +interaction action=hover repeat=1 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.693916ms +interaction action=review-c repeat=1 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=2.467208ms +interaction action=mark-space repeat=2 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.570084ms +interaction action=hover repeat=2 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.661209ms +interaction action=review-c repeat=2 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=2.573834ms +interaction action=mark-space repeat=3 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.588833ms +interaction action=hover repeat=3 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.671125ms +interaction action=review-c repeat=3 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=2.675084ms +interaction action=mark-space repeat=1 marks=8->9 renders=3 prepares=3 plans=6 refreshes=0 wall=5.180041ms +interaction action=hover repeat=1 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.608791ms +interaction action=review-c repeat=1 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=4.043208ms +interaction action=mark-space repeat=2 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.50525ms +interaction action=hover repeat=2 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.503375ms +interaction action=review-c repeat=2 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=4.018792ms +interaction action=mark-space repeat=3 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.820917ms +interaction action=hover repeat=3 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=4.015208ms +interaction action=review-c repeat=3 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=4.169958ms +interaction action=mark-space repeat=1 marks=32->33 renders=3 prepares=3 plans=6 refreshes=0 wall=7.792416ms +interaction action=hover repeat=1 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=5.2135ms +interaction action=review-c repeat=1 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=8.745875ms +interaction action=mark-space repeat=2 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=5.241834ms +interaction action=hover repeat=2 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=5.400709ms +interaction action=review-c repeat=2 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=8.7555ms +interaction action=mark-space repeat=3 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=5.447334ms +interaction action=hover repeat=3 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=5.865541ms +interaction action=review-c repeat=3 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=9.197958ms +interaction action=mark-space repeat=1 marks=128->129 renders=3 prepares=3 plans=6 refreshes=0 wall=19.085708ms +interaction action=hover repeat=1 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=13.022875ms +interaction action=review-c repeat=1 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=30.691375ms +interaction action=mark-space repeat=2 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=14.717709ms +interaction action=hover repeat=2 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=14.473875ms +interaction action=review-c repeat=2 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=30.634125ms +interaction action=mark-space repeat=3 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=12.734667ms +interaction action=hover repeat=3 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=12.695834ms +interaction action=review-c repeat=3 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=30.7185ms +interaction action=mark-space repeat=1 marks=1024->1025 renders=3 prepares=3 plans=6 refreshes=0 wall=113.33325ms +interaction action=hover repeat=1 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=94.280958ms +interaction action=review-c repeat=1 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=247.047292ms +interaction action=mark-space repeat=2 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=76.356875ms +interaction action=hover repeat=2 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=74.409208ms +interaction action=review-c repeat=2 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=242.707958ms +interaction action=mark-space repeat=3 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=77.313917ms +interaction action=hover repeat=3 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=74.570667ms +interaction action=review-c repeat=3 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=237.816292ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 78 filtered out; finished in 3.16s + diff --git a/docs/performance/mark-resolution/interactions-red.log b/docs/performance/mark-resolution/interactions-red.log new file mode 100644 index 00000000..a913f804 --- /dev/null +++ b/docs/performance/mark-resolution/interactions-red.log @@ -0,0 +1,66 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Finished `test` profile [optimized + debuginfo] target(s) in 3.91s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +test tests::marked_interaction_timings ... interaction action=mark-space repeat=1 marks=0->1 renders=3 prepares=3 plans=6 refreshes=0 wall=3.866334ms +interaction action=hover repeat=1 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.832708ms +interaction action=review-c repeat=1 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=2.252292ms +interaction action=mark-space repeat=2 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.443ms +interaction action=hover repeat=2 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.800667ms +interaction action=review-c repeat=2 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=2.211042ms +interaction action=mark-space repeat=3 marks=0->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.408125ms +interaction action=hover repeat=3 marks=1->1 renders=2 prepares=2 plans=4 refreshes=0 wall=2.487209ms +interaction action=review-c repeat=3 marks=1->1 renders=3 prepares=1 plans=4 refreshes=0 wall=2.212542ms +interaction action=mark-space repeat=1 marks=1->2 renders=3 prepares=3 plans=6 refreshes=0 wall=3.834833ms +interaction action=hover repeat=1 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.865791ms +interaction action=review-c repeat=1 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=2.445292ms +interaction action=mark-space repeat=2 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.74825ms +interaction action=hover repeat=2 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.735875ms +interaction action=review-c repeat=2 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=2.421667ms +interaction action=mark-space repeat=3 marks=1->2 renders=2 prepares=2 plans=4 refreshes=0 wall=3.28325ms +interaction action=hover repeat=3 marks=2->2 renders=2 prepares=2 plans=4 refreshes=0 wall=2.782625ms +interaction action=review-c repeat=3 marks=2->2 renders=3 prepares=1 plans=4 refreshes=0 wall=2.733167ms +interaction action=mark-space repeat=1 marks=8->9 renders=3 prepares=3 plans=6 refreshes=0 wall=5.291709ms +interaction action=hover repeat=1 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=4.145167ms +interaction action=review-c repeat=1 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=4.556709ms +interaction action=mark-space repeat=2 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.612542ms +interaction action=hover repeat=2 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.624666ms +interaction action=review-c repeat=2 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=4.150625ms +interaction action=mark-space repeat=3 marks=8->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.549459ms +interaction action=hover repeat=3 marks=9->9 renders=2 prepares=2 plans=4 refreshes=0 wall=3.6055ms +interaction action=review-c repeat=3 marks=9->9 renders=3 prepares=1 plans=4 refreshes=0 wall=4.159167ms +interaction action=mark-space repeat=1 marks=32->33 renders=3 prepares=3 plans=6 refreshes=0 wall=9.137625ms +interaction action=hover repeat=1 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=6.42875ms +interaction action=review-c repeat=1 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=9.801417ms +interaction action=mark-space repeat=2 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=6.260209ms +interaction action=hover repeat=2 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=6.274417ms +interaction action=review-c repeat=2 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=9.739625ms +interaction action=mark-space repeat=3 marks=32->33 renders=2 prepares=2 plans=4 refreshes=0 wall=6.120542ms +interaction action=hover repeat=3 marks=33->33 renders=2 prepares=2 plans=4 refreshes=0 wall=6.348584ms +interaction action=review-c repeat=3 marks=33->33 renders=3 prepares=1 plans=4 refreshes=0 wall=9.569958ms +interaction action=mark-space repeat=1 marks=128->129 renders=3 prepares=3 plans=6 refreshes=0 wall=41.574ms +interaction action=hover repeat=1 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=29.242583ms +interaction action=review-c repeat=1 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=44.395917ms +interaction action=mark-space repeat=2 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=27.956334ms +interaction action=hover repeat=2 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=27.605834ms +interaction action=review-c repeat=2 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=44.052416ms +interaction action=mark-space repeat=3 marks=128->129 renders=2 prepares=2 plans=4 refreshes=0 wall=29.216375ms +interaction action=hover repeat=3 marks=129->129 renders=2 prepares=2 plans=4 refreshes=0 wall=27.679958ms +interaction action=review-c repeat=3 marks=129->129 renders=3 prepares=1 plans=4 refreshes=0 wall=44.189917ms +interaction action=mark-space repeat=1 marks=1024->1025 renders=3 prepares=3 plans=6 refreshes=0 wall=1.726271166s +interaction action=hover repeat=1 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.09936375s +interaction action=review-c repeat=1 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=1.237629458s +interaction action=mark-space repeat=2 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.154407584s +interaction action=hover repeat=2 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.109142875s +interaction action=review-c repeat=2 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=1.229517333s +interaction action=mark-space repeat=3 marks=1024->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.064909416s +interaction action=hover repeat=3 marks=1025->1025 renders=2 prepares=2 plans=4 refreshes=0 wall=1.068550917s +interaction action=review-c repeat=3 marks=1025->1025 renders=3 prepares=1 plans=4 refreshes=0 wall=1.224605875s +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 26.34s + diff --git a/docs/performance/mark-resolution/interactions.md b/docs/performance/mark-resolution/interactions.md new file mode 100644 index 00000000..427e1b78 --- /dev/null +++ b/docs/performance/mark-resolution/interactions.md @@ -0,0 +1,96 @@ +# What is a mark, and when does the delay happen? + +This file preserves the first set of input measurements at the original +PR #9 fix. For the final combined scope, current validation and freshly +replayed main-versus-final inputs, see [expanded evidence](expanded/README.md). + +A **mark** is one file or directory queued for removal. Marking a directory +marks that path once, regardless of how many files it contains. For example, +marking a cache directory containing a million files is M=1, not M=1,000,000. +Marking 1,024 unrelated paths means 1,024 separate queued targets. Main has no +usage telemetry establishing that this is a frequent manual workload; it is +a large-queue stress case, also relevant to programmatic/bulk control. + +The earlier **6.60 seconds** measured **four forced window draws, which caused +eight `prepare` calls** in the GPUI harness. It was not a scan time, a single +click time, or a measured delay for every item. The ~359 ms fixed result has +the same denominator. These draws were of the Explore screen with many marks +already present; they were not the act of selecting 1,024 items. + +## Where it repeats + +- Explore calls `prepare` whenever the app renders, resolving marked paths on + main. Its Marked section and disk projection each call `app.plan()` when + those panels are shown, evaluating the entire queue again. +- Marking/unmarking, keyboard selection, zoom and pointer movement over a + tile can request another render. Pointer movement within the same hovered + tile also requests rendering because the tooltip follows the pointer. +- Opening Review calls `app.plan()` for the queue again. Review does not use + the treemap's `prepare`, but it still pays the original quadratic removal + nesting cost, plus constructing the list rows. +- `Marks::refresh` is different: it runs when a scan result arrives and when + the metric changes. It is not run by every mark action or hover. +- An unchanged idle window need not redraw. The free-space ticker checks + every 1.2 seconds outside removal, but only requests rendering if the value + changes. This is not an unconditional six-second periodic stall. + +## Individual user actions, with smaller queues + +An additional opt-in real-window test measures actual Space, pointer-move and +C inputs. The fixture exists on disk: 1,025 sibling directories, each with one +64-byte file, scanned before timing. The tree size stays fixed at 2,050 +non-root nodes while the queued count changes. The window is 1,400 by 900, +with the selection/Marked panel and disk projection visible. No files are +deleted or trashed. + +Each row seeds M folder marks, then measures adding one more folder with +Space. Hover and Review are measured with M+1 marks afterward. Values are +medians of three input repetitions in milliseconds; each cell is +**main → fixed**. Both snapshots use the same fixture and test-only counters. + +| Already queued M | Add one mark (Space) | Move pointer over a tile | Open Review (C) | +| ---: | ---: | ---: | ---: | +| 0 | 2.44 → 2.51 | 2.80 → 2.55 | 2.21 → 2.18 | +| 1 | 3.28 → 2.59 | 2.78 → 2.67 | 2.45 → 2.57 | +| 8 | 3.61 → 3.82 | 3.62 → 3.61 | 4.16 → 4.04 | +| 32 | 6.26 → 5.45 | 6.35 → 5.40 | 9.74 → 8.76 | +| 128 | 29.22 → 14.72 | 27.68 → 13.02 | 44.19 → 30.69 | +| 1,024 | 1,154.41 → 77.31 | 1,099.36 → 74.57 | 1,229.52 → 242.71 | + +For a handful of marks, these operations were already a few milliseconds on +main; this measurement does not establish a substantial common-case speedup. +At 128 marks the repeated work is more noticeable, and the large queue causes +roughly a one-second delay for ordinary interactions in this fixture. The +problem repeats while interacting with that queue, rather than being paid +once when the scan finishes. Opening a large Review list remains more costly +than hovering because it constructs many rows as well as evaluating the plan. + +The measured Space input caused two renders in repetitions 2–3, and three in +the first repetition. Hover caused two renders. Review caused three renders +(one Explore prepare plus the switch to Review). With panels visible, these +inputs evaluated four plans, or six for the first Space repetition. Framework +render scheduling can vary, so these are end-to-end harness input timings, +not a universal mapping of one physical event to one frame. + +Every timed input asserts that real rendering and removal projections ran, +and that **zero `Marks::refresh` calls** occurred. No scan is part of these +individual-action numbers. Raw output includes exact render, prepare, plan +and refresh counts: + +- [Main interaction log](interactions-red.log): main algorithms from the + red commit `5a3c92e`, with the new test/counters temporarily applied. +- [Fixed interaction log](interactions-green.log): fixed algorithms with + the identical test/counters. + +These are synthetic macOS arm64 test-profile measurements, not release-app +latency guarantees or evidence of how often users select a given queue size. +The previous deterministic N/8N budgets remain necessary to protect scaling; +this additional test is intentionally ignored by normal CI because elapsed +latency is recorded as evidence, not asserted as a portable threshold. + +Reproduce on the final branch: + +```sh +cargo test -p disktree-app marked_interaction_timings -- \ + --ignored --nocapture --test-threads=1 +``` diff --git a/docs/performance/mark-resolution/lint.log b/docs/performance/mark-resolution/lint.log new file mode 100644 index 00000000..1687dd61 --- /dev/null +++ b/docs/performance/mark-resolution/lint.log @@ -0,0 +1,6 @@ + Finished `dev` profile [optimized + debuginfo] target(s) in 0.09s + Running `target/debug/xtask lint` + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `dev` profile [optimized + debuginfo] target(s) in 1.10s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` diff --git a/docs/performance/mark-resolution/red-core.log b/docs/performance/mark-resolution/red-core.log new file mode 100644 index 00000000..2227d991 --- /dev/null +++ b/docs/performance/mark-resolution/red-core.log @@ -0,0 +1,33 @@ + Compiling fastrand v2.5.0 + Compiling either v1.18.0 + Compiling log v0.4.34 + Compiling num-traits v0.2.19 + Compiling rustix v1.1.4 + Compiling once_cell v1.21.4 + Compiling errno v0.3.14 + Compiling objc2-foundation v0.3.2 + Compiling rayon v1.12.0 + Compiling trash v5.2.9 + Compiling chrono v0.4.45 + Compiling tempfile v3.27.0 + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Finished `test` profile [optimized + debuginfo] target(s) in 3.82s + Running unittests src/lib.rs (target/debug/deps/disktree_core-c3cb863045931ea5) + +running 1 test +plan M=128 work=9187 wall=9.931833ms +plan M=1024 work=537027 wall=390.640459ms + +thread 'removal::tests::removal_nesting_budget' (403531013) panicked at crates/disktree-core/src/removal.rs:1789:9: +8x marks: 9187 -> 537027 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +test removal::tests::removal_nesting_budget ... FAILED + +failures: + +failures: + removal::tests::removal_nesting_budget + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 149 filtered out; finished in 0.40s + +error: test failed, to rerun pass `-p disktree-core --lib` diff --git a/docs/performance/mark-resolution/red-dimensions.log b/docs/performance/mark-resolution/red-dimensions.log new file mode 100644 index 00000000..4d93cf97 --- /dev/null +++ b/docs/performance/mark-resolution/red-dimensions.log @@ -0,0 +1,17 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Finished `test` profile [optimized + debuginfo] target(s) in 5.10s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 1 test +dimensions folders=128 M=8 D=2 R=1 work=1012 wall=61.958µs +dimensions folders=1024 M=8 D=2 R=1 work=8180 wall=34.958µs +dimensions folders=1024 M=64 D=2 R=1 work=63648 wall=284.042µs +dimensions folders=128 M=8 D=16 R=1 work=1124 wall=10.666µs +dimensions folders=128 M=8 D=2 R=8 work=8096 wall=46.792µs +test marks::tests::refresh_dimension_budget ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.00s + diff --git a/docs/performance/mark-resolution/red.log b/docs/performance/mark-resolution/red.log new file mode 100644 index 00000000..75475620 --- /dev/null +++ b/docs/performance/mark-resolution/red.log @@ -0,0 +1,32 @@ + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 5.18s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 4 tests +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +refresh M=128 N=256 R=4 work=34048 wall=364µs +refresh M=1024 N=2048 R=4 work=2107392 wall=8.818542ms + +thread 'marks::tests::refresh_resolution_budget' (403532227) panicked at crates/disktree-app/src/marks.rs:208:9: +8x marks/folders: 34048 -> 2107392 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +test marks::tests::refresh_resolution_budget ... FAILED +window M=128 N=256 draws=4 prepares=8 work=68312 wall=171.271792ms +window M=1024 N=2048 draws=4 prepares=8 work=4215000 wall=6.734163791s + +thread 'tests::marked_window_resolution_budget' (403532228) panicked at crates/disktree-app/src/tests.rs:1904:5: +8x marks/folders per frame: 68312/8 -> 4215000/8 +test tests::marked_window_resolution_budget ... FAILED + +failures: + +failures: + marks::tests::refresh_resolution_budget + tests::marked_window_resolution_budget + +test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 69 filtered out; finished in 6.96s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` diff --git a/docs/performance/mark-resolution/tests.log b/docs/performance/mark-resolution/tests.log new file mode 100644 index 00000000..af929fb7 --- /dev/null +++ b/docs/performance/mark-resolution/tests.log @@ -0,0 +1,265 @@ + Finished `dev` profile [optimized + debuginfo] target(s) in 0.08s + Running `target/debug/xtask test` + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 4.25s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 79 tests +test appearance::tests::a_linux_desktop_is_followed_only_if_it_states_a_preference ... ok +test git::tests::summaries_say_what_would_be_lost ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::toggling_marks_and_unmarks ... ok +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test marks::tests::display_path_shortens_the_home_prefix ... ok +test marks::tests::marking_the_same_path_twice_keeps_one_entry ... ok +test marks::tests::resolver_preserves_lossy_non_utf8_components ... ok +test palette::tests::every_legend_category_has_its_own_hue ... ok +test palette::tests::age_buckets_cover_every_age_in_order ... ok +test palette::tests::mix_clamps_its_parameter ... ok +test palette::tests::colourful_categories_share_one_level ... ok +test palette::tests::deeper_tiles_lift_away_from_the_background ... ok +test palette::tests::mixing_toward_a_grey_keeps_the_hue ... ok +test palette::tests::the_highlight_is_not_a_category_colour ... ok +test power::tests::presets_are_fixed_and_show_the_effective_cpu_limit ... ok +test power::tests::presets_that_add_no_workers_are_not_offered ... ok +test state::tests::a_transition_grows_a_tile_that_is_being_entered ... ok +test state::tests::a_transition_starts_where_the_region_was_and_ends_where_it_is ... ok +test marks::tests::refresh_re_reads_sizes_from_a_new_tree ... ok +test state::tests::clamping_never_leaves_a_margin ... ok +test marks::tests::find_matches_whole_components_only ... ok +test marks::tests::resolver_preserves_scan_name_semantics ... ok +test marks::tests::cached_crumbs_follow_every_invalidation_boundary ... ok +test state::tests::directions_only_see_gaps_on_their_own_side ... ok +test argument_tests::saved_power_is_loaded_before_cli_overrides ... ok +test state::tests::projecting_and_unprojecting_are_inverses ... ok +test state::tests::zooming_keeps_the_point_under_the_cursor_still ... ok +test marks::tests::refresh_dimension_budget ... ok +test power::tests::saved_choice_survives_replacement_and_bad_data_is_reported ... ok +test marks::tests::refresh_resolution_budget ... ok +test git::tests::reads_a_real_checkout ... ok +test git::tests::a_checkout_cannot_make_git_run_its_programs ... ok +test git::tests::a_checkout_cannot_make_git_verify_a_signature_with_its_program ... ok +test git::tests::a_checkouts_own_filter_is_never_run ... ok +test tests::command_chords_are_not_read_as_plain_letters ... ok +test tests::marked_interaction_timings ... ignored, manual interaction timing on synthetic temporary files +test tests::enter_before_the_search_lands_applies_it_when_it_does ... ok +test tests::escape_in_the_delete_dialog_cancels ... ok +test tests::a_view_without_flags_or_marks_still_draws ... ok +test tests::back_and_forward_retrace_where_you_have_been ... ok +test tests::interface_zoom_scales_the_rem_and_the_header_band ... ok +test tests::restart_arguments_parse_back_to_the_same_scan ... ok +test tests::after_descending_every_tile_is_inside_the_directory_drawn ... ok +test tests::enter_opens_the_selected_directory_at_any_depth ... ok +test tests::escape_cancels_the_first_scan_and_r_starts_it_again ... ok +test tests::enter_in_the_focused_volume_picker_scans_the_selected_root ... ok +test tests::a_permanent_deletion_asks_in_an_alert_dialog_then_removes ... ok +test tests::escape_cancels_widening_and_keeps_the_tree_on_screen ... ok +test tests::keys_walk_the_tree_and_mark_what_is_selected ... ok +test tests::a_crumb_lists_its_siblings_and_jumps_sideways ... ok +test tests::hovering_reports_the_tile_under_the_pointer ... ok +test tests::a_parents_name_gets_its_own_band_above_its_children ... ok +test tests::dragging_the_panel_edge_resizes_it_within_its_limits ... ok +test tests::both_appearances_draw ... ok +test tests::marked_tiles_follow_metric_rescan_and_mark_changes ... ok +test tests::the_help_overlay_opens_and_closes ... ok +test tests::the_review_screen_switches_removal_mode ... ok +test tests::the_review_screen_copies_the_list_as_an_agent_prompt ... ok +test tests::switching_the_metric_keeps_the_directory_and_the_selection ... ok +test widgets::tests::ages_read_in_the_unit_a_person_would_use ... ok +test widgets::tests::sizes_split_into_number_and_unit ... ok +test tests::right_click_selects_the_tile_and_reveals_it ... ok +test tests::showing_a_tile_that_is_gone_says_so_instead ... ok +test tests::power_efficiency_menu_saves_without_discarding_the_tree ... ok +test tests::mouse_side_buttons_go_back_and_forward ... ok +test tests::marking_a_directory_marks_everything_inside_it ... ok +test tests::the_first_scan_shows_what_it_is_doing_then_the_treemap ... ok +test tests::the_trash_is_the_default_when_there_is_one ... ok +test tests::the_mark_key_follows_the_pointer_until_the_keyboard_moves ... ok +test tests::the_power_menu_offers_only_what_the_cpus_can_tell_apart ... ok +test tests::the_volume_picker_is_centred_and_still_dismisses_from_outside ... ok +test tests::the_treemap_zooms_with_the_wheel_and_resets ... ok +test tests::the_volume_picker_opens_moves_and_closes ... ok +test tests::the_window_draws_a_treemap_with_tiles ... ok +test tests::the_wheel_goes_into_the_directory_it_zoomed_into ... ok +test tests::widening_reuses_the_tree_it_has_and_reads_only_the_rest ... ok +test tests::typing_filters_live_and_enter_shows_only_the_matches ... ok +test tests::marked_window_resolution_budget ... ok + +test result: ok. 78 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.46s + + Running unittests src/lib.rs (target/debug/deps/disktree_core-9300e5ba20d6b25f) + +running 152 tests +test classify::tests::steam_libraries_and_emulation_are_media_not_disposable_caches ... ok +test classify::tests::macos_developer_leftovers_are_reclaimable_and_its_risks_are_not ... ok +test classify::tests::windows_caches_and_sync_folders_are_recognized ... ok +test classify::tests::the_legend_lists_every_named_category_once ... ok +test filter::tests::case_is_ignored_without_allocating ... ok +test export::tests::the_list_is_one_path_per_line ... ok +test export::tests::a_name_with_a_newline_cannot_smuggle_in_a_path ... ok +test export::tests::a_name_that_is_not_unicode_is_flagged_not_listed ... ok +test export::tests::the_prompt_lists_every_path_with_its_size_and_the_rules ... ok +test access::tests::only_macos_has_an_answer_and_a_missing_database_is_not_a_refusal ... ok +test filter::tests::matches_are_kept_whole_and_their_ancestors_by_what_matched ... ok +test filter::tests::a_search_below_the_root_leaves_the_rest_alone ... ok +test classify::tests::a_steam_library_on_another_drive_is_media_not_its_temp_folder ... ok +test insights::tests::ranks_findings_largest_first_and_skips_the_tiny ... ok +test insights::tests::documents_are_never_suggested ... ok +test filter::tests::an_empty_needle_filters_nothing ... ok +test insights::tests::the_limit_keeps_the_largest ... ok +test classify::tests::an_unknown_top_level_directory_takes_its_largest_known_child ... ok +test classify::tests::caches_are_reclaimable_all_the_way_down ... ok +test insights::tests::crumbs_address_the_finding_from_the_root ... ok +test classify::tests::names_announce_their_kind_and_children_inherit_it ... ok +test classify::tests::target_is_build_output_only_beside_a_cargo_manifest ... ok +test removal::tests::a_directory_holding_a_system_tree_is_refused ... ok +test removal::tests::a_directory_holding_the_home_directory_is_refused ... ok +test classify::tests::sandbox_layers_and_snapshots_are_reclaimable_only_in_sandbox_state ... ok +test macos::tests::a_missing_directory_is_an_error ... ok +test removal::tests::macos_always_uses_the_system_trash ... ok +test removal::tests::macos_guard_keys_fold_the_data_volume_and_case ... ok +test removal::tests::macos_guards_see_through_other_spellings ... ok +test removal::tests::a_name_that_is_not_utf8_is_refused_before_the_trash_sees_it ... ok +test removal::tests::macos_system_trees_are_refused_but_apps_are_not ... ok +test removal::tests::names_win32_would_misread_are_recognized ... ok +test macos::tests::a_listing_has_names_kinds_lengths_and_no_dot_entries ... ok +test removal::tests::nesting_keeps_component_boundaries_duplicates_and_output_order ... ok +test removal::tests::normalize_resolves_dots_without_the_filesystem ... ok +test removal::tests::nesting_uses_guard_case_order_even_when_the_child_sorts_first ... ok +test macos::tests::names_are_text_and_open ... ok +test removal::tests::a_mount_below_a_target_is_caught_again_at_removal_time ... ok +test removal::tests::percent_encoding_escapes_what_the_spec_requires ... ok +test removal::tests::a_volume_is_judged_by_its_mount_where_the_kernel_names_one ... ok +test removal::tests::a_missing_target_reports_not_found ... ok +test removal::tests::a_plan_keeps_only_the_outermost_targets ... ok +test removal::tests::mount_points_are_recognized ... ok +test macos::tests::every_leaf_reads_as_lstat_reads_it ... ok +test removal::tests::a_target_with_a_mount_inside_is_refused ... ok +test removal::tests::a_trash_info_path_is_restorable ... ok +test removal::tests::paths_outside_the_root_are_refused ... ok +test removal::tests::a_target_reached_through_a_link_is_refused ... ok +test removal::tests::a_removal_run_reports_every_item_and_a_total ... ok +test removal::tests::permanent_removal_takes_directories_and_leaves_siblings ... ok +test removal::tests::the_macos_trash_takes_a_file_and_leaves_a_linked_target ... ignored, puts a file in this Mac's real Trash +test removal::tests::home_is_found_by_its_real_path_too ... ok +test removal::tests::permanent_removal_follows_links_above_the_root ... ok +test removal::tests::nothing_is_mounted_below_a_temp_dir ... ok +test removal::tests::permanent_removal_unlinks_a_symlink_instead_of_following_it ... ok +test removal::tests::permanent_removal_unlinks_nested_symlinks_without_following_them ... ok +test removal::tests::read_only_files_are_removed_too ... ok +test removal::tests::permanent_removal_takes_nested_trees_and_hidden_files ... ok +test removal::tests::permanent_removal_does_not_follow_a_link_put_on_the_way_down ... ok +test removal::tests::a_link_put_on_the_way_down_after_planning_stops_the_worker ... ok +test removal::tests::trash_names_avoid_collisions ... ok +test removal::tests::the_root_and_home_are_refused ... ok +test removal::tests::sibling_prefixes_are_not_treated_as_containment ... ok +test removal::tests::the_mount_table_is_read_off_the_calling_thread_and_then_kept ... ok +test scan::tests::a_file_root_is_rejected ... ok +test scan::tests::fixed_worker_pools_change_size_without_reinitializing_rayon ... ok +test removal::tests::the_xdg_trash_moves_a_file_and_records_where_it_came_from ... ok +test scan::tests::a_directory_reports_its_own_bytes_separately ... ok +test removal::tests::this_macs_mount_table_is_read_without_proc ... ok +test scan::tests::an_unreadable_directory_is_recorded_not_fatal ... ok +test scan::tests::disk_usage_is_whole_allocation_units ... ok +test scan::tests::children_are_ranked_largest_first ... ok +test scan::tests::a_spawned_scan_reports_progress_and_a_result ... ok +test scan::tests::whole_disk_smoke ... ignored, walks the whole disk +test scan::tests::hardlinks_are_charged_once_by_default ... ok +test scan_threads::tests::governor_waits_for_retirement_and_recovers_without_exceeding_target ... ok +test scan_threads::tests::inconclusive_windows_cannot_accept_a_reduction ... ok +test scan::tests::hidden_entries_are_included_by_default_and_excluded_on_request ... ok +test scan_threads::tests::increased_limit_is_not_supplied_until_jobs_are_dispatched ... ok +test scan_threads::tests::long_directory_never_becomes_a_fully_supplied_window ... ok +test scan::tests::followed_symlink_loops_do_not_hang_the_scan ... ok +test scan_threads::tests::search_refines_one_reference_without_compounding_loss ... ok +test size::tests::a_value_that_rounds_up_to_the_next_unit_is_shown_in_it ... ok +test size::tests::bytes_use_binary_units_with_one_decimal_below_ten ... ok +test size::tests::counts_switch_to_metric_suffixes ... ok +test size::tests::share_bar_is_always_the_requested_width ... ok +test size::tests::share_survives_a_zero_total ... ok +test size::tests::short_form_drops_the_space ... ok +test space::tests::a_home_scan_has_no_foreign_mounts_here ... ok +test space::tests::a_bind_mount_inside_the_scan_is_left_out ... ok +test scan::tests::a_wider_scan_reuses_the_subtree_it_already_knows ... ok +test space::tests::a_missing_path_reports_the_io_error ... ok +test space::tests::a_real_volume_reports_plausible_numbers ... ok +test space::tests::a_root_booted_from_a_snapshot_keeps_its_subvolumes ... ok +test space::tests::a_table_that_does_not_place_a_path_claims_nothing ... ok +test space::tests::a_volume_includes_its_subvolumes_and_nothing_else ... ok +test space::tests::a_volume_keeps_its_subvolumes_and_loses_everything_else ... ok +test scan::tests::max_depth_stops_descending_but_keeps_direct_files ... ok +test space::tests::btrfs_subvolume_devices_can_differ ... ok +test space::tests::excluded_views_cannot_replace_visible_files ... ok +test space::tests::hidden_original_and_nested_mounts_are_preserved ... ok +test scan_threads::tests::real_pool_retires_at_job_boundaries_and_drains_queued_descendants ... ok +test space::tests::macos_mount_points_keep_spaces_and_brackets ... ok +test space::tests::mountinfo_escapes_are_decoded_once ... ok +test space::tests::only_macos_skips_the_data_volume_and_in_the_roots_spelling ... ok +test space::tests::projecting_removal_cannot_exceed_the_volume ... ok +test space::tests::separate_home_disk_is_its_own_volume ... ok +test space::tests::subvolumes_are_not_repeats_but_a_second_mount_of_a_disk_is ... ok +test space::tests::the_device_is_the_longest_matching_mount ... ok +test space::tests::the_home_disk_on_macos_is_the_root_and_has_a_device ... ok +test space::tests::the_whole_disk_is_the_top_of_the_home_volume ... ok +test space::tests::the_top_of_a_btrfs_disk_beside_its_subvolumes_is_counted_once ... ok +test space::tests::unrelated_tmpfs_mounts_are_not_duplicates ... ok +test space::tests::used_fraction_handles_an_empty_volume_report ... ok +test space::tests::volumes_with_least_space_come_first_and_unknowns_last ... ok +test space::tests::volume_candidates_are_real_disks_not_pseudo_filesystems ... ok +test scan::tests::symlinks_are_not_followed_by_default ... ok +test tree::tests::a_node_stays_small ... ok +test tree::tests::depth_counts_edges ... ok +test tree::tests::aggregate_derives_totals_and_orders_children ... ok +test tree::tests::find_reports_crumbs_and_is_case_insensitive ... ok +test tree::tests::identities_that_differ_only_in_their_top_bits_are_two_files ... ok +test tree::tests::largest_child_is_the_first_child_because_children_are_sorted ... ok +test tree::tests::path_of_joins_names_beneath_the_root ... ok +test tree::tests::resolve_walks_child_indices ... ok +test tree::tests::aggregate_can_rank_by_file_count ... ok +test treemap::tests::a_leaf_never_claims_a_header ... ok +test treemap::tests::a_filtered_layout_shows_only_the_matches_at_their_size ... ok +test treemap::tests::a_tile_without_room_for_a_header_stays_whole ... ok +test treemap::tests::a_subdivided_directory_keeps_a_header_for_its_own_name ... ok +test treemap::tests::hit_returns_the_deepest_tile ... ok +test treemap::tests::layout_depth_one_stops_at_direct_children ... ok +test treemap::tests::layout_merges_the_child_tail_into_one_tile ... ok +test treemap::tests::squarify_fills_the_area ... ok +test treemap::tests::squarify_keeps_aspect_ratios_reasonable ... ok +test treemap::tests::layout_nests_children_inside_their_parent ... ok +test treemap::tests::squarify_survives_degenerate_input ... ok +test treemap::tests::tile_crumbs_address_the_scanned_tree_not_the_drawn_node ... ok +test scan::tests::totals_are_summed_bottom_up ... ok +test space::tests::without_a_table_the_same_place_is_one_volume ... ok +test tree::tests::seen_charges_an_identity_once_in_either_store ... ok +test scan::tests::file_count_metric_ranks_by_entries ... ok +test removal::tests::system_trees_are_refused_in_a_whole_disk_scan ... ok +test removal::tests::the_directories_above_home_are_refused ... ok +test removal::tests::removal_nesting_budget ... ok +test scan::tests::cancelled_queued_jobs_release_parent_completion_tokens ... ok +test removal::tests::a_failing_trash_tool_reports_its_stderr ... ok +test scan::tests::fixed_worker_pools_preserve_a_wide_deep_tree ... ok +test removal::tests::only_the_scanned_volumes_marks_are_projected ... ok +test scan_threads::tests::controller_retires_recovers_after_search_and_cancels ... ok +test macos::tests::a_directory_larger_than_the_buffer_is_listed_whole ... ok +test removal::tests::a_trash_tool_is_called_with_the_path_after_a_separator ... ok + +test result: ok. 150 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.20s + + Running unittests src/main.rs (target/debug/deps/xtask-0dc2df263f51d89e) + +running 3 tests +test bundle::tests::signing_is_ad_hoc_unless_an_identity_is_given ... ok +test bundle::tests::notarizing_needs_a_real_signature ... ok +test bundle::tests::the_plist_template_names_the_executable_and_takes_the_version ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Doc-tests disktree_core + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + diff --git a/docs/performance/mark-resolution/wall-green-repeats.log b/docs/performance/mark-resolution/wall-green-repeats.log new file mode 100644 index 00000000..ece27470 --- /dev/null +++ b/docs/performance/mark-resolution/wall-green-repeats.log @@ -0,0 +1,126 @@ + +REPEAT 1: disktree-app fixed + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Finished `test` profile [optimized + debuginfo] target(s) in 5.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 5 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=92.541µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=47.167µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=50.167µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=28.084µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=57.541µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=204.417µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.528584ms +ok +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=64.363667ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=66.748208ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=347.242958ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=358.680958ms +ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.91s + +EXIT 0 + +REPEAT 1: disktree-core fixed + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Finished `test` profile [optimized + debuginfo] target(s) in 1.68s + Running unittests src/lib.rs (target/debug/deps/disktree_core-c3cb863045931ea5) + +running 1 test +test removal::tests::removal_nesting_budget ... plan M=128 work=1314 wall=3.372625ms +plan M=1024 work=15298 wall=23.837875ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 151 filtered out; finished in 0.03s + +EXIT 0 + +REPEAT 2: disktree-app fixed + Finished `test` profile [optimized + debuginfo] target(s) in 0.29s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 5 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=23.458µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=44.542µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=56.5µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=26µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=55.125µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=208.667µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.52875ms +ok +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=65.17375ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=67.732166ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=350.850083ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=356.051625ms +ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.89s + +EXIT 0 + +REPEAT 2: disktree-core fixed + Finished `test` profile [optimized + debuginfo] target(s) in 0.10s + Running unittests src/lib.rs (target/debug/deps/disktree_core-c3cb863045931ea5) + +running 1 test +test removal::tests::removal_nesting_budget ... plan M=128 work=1314 wall=2.852625ms +plan M=1024 work=15298 wall=21.89725ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 151 filtered out; finished in 0.03s + +EXIT 0 + +REPEAT 3: disktree-app fixed + Finished `test` profile [optimized + debuginfo] target(s) in 0.30s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 5 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_dimension_budget ... dimensions folders=128 M=8 D=2 R=1 work=160 wall=23µs +dimensions folders=1024 M=8 D=2 R=1 work=1056 wall=51.5µs +dimensions folders=1024 M=64 D=2 R=1 work=1280 wall=59µs +dimensions folders=128 M=8 D=16 R=1 work=384 wall=33.667µs +dimensions folders=128 M=8 D=2 R=8 work=1280 wall=60.167µs +ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=2560 wall=198.958µs +refresh M=1024 N=2048 R=4 work=20480 wall=1.648333ms +ok +test tests::marked_window_resolution_budget ... warm window M=128 prepares=8 resolution=216 tiles=1544 wall=65.083791ms +window M=128 N=256 draws=4 prepares=8 work=856 wall=66.946292ms +warm window M=1024 prepares=8 resolution=216 tiles=776 wall=340.564ms +window M=1024 N=2048 draws=4 prepares=8 work=5336 wall=359.560417ms +ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.89s + +EXIT 0 + +REPEAT 3: disktree-core fixed + Finished `test` profile [optimized + debuginfo] target(s) in 0.11s + Running unittests src/lib.rs (target/debug/deps/disktree_core-c3cb863045931ea5) + +running 1 test +test removal::tests::removal_nesting_budget ... plan M=128 work=1314 wall=3.403792ms +plan M=1024 work=15298 wall=23.242542ms +ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 151 filtered out; finished in 0.03s + +EXIT 0 diff --git a/docs/performance/mark-resolution/wall-red-repeats.log b/docs/performance/mark-resolution/wall-red-repeats.log new file mode 100644 index 00000000..55f720af --- /dev/null +++ b/docs/performance/mark-resolution/wall-red-repeats.log @@ -0,0 +1,174 @@ + +REPEAT 1: disktree-app baseline 5a3c92e + Compiling disktree-app v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-app) + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Finished `test` profile [optimized + debuginfo] target(s) in 4.81s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 4 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=34048 wall=457.125µs +refresh M=1024 N=2048 R=4 work=2107392 wall=11.105667ms + +thread 'marks::tests::refresh_resolution_budget' (403559694) panicked at crates/disktree-app/src/marks.rs:208:9: +8x marks/folders: 34048 -> 2107392 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED +test tests::marked_window_resolution_budget ... window M=128 N=256 draws=4 prepares=8 work=68312 wall=159.687834ms +window M=1024 N=2048 draws=4 prepares=8 work=4215000 wall=6.601997625s + +thread 'tests::marked_window_resolution_budget' (403559695) panicked at crates/disktree-app/src/tests.rs:1904:5: +8x marks/folders per frame: 68312/8 -> 4215000/8 +FAILED + +failures: + +failures: + marks::tests::refresh_resolution_budget + tests::marked_window_resolution_budget + +test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 69 filtered out; finished in 6.83s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` +EXIT 101 + +REPEAT 1: disktree-core baseline 5a3c92e + Compiling disktree-core v0.11.0 (/Users/huntharo/.codex/worktrees/mv8epstr/disktree/crates/disktree-core) + Finished `test` profile [optimized + debuginfo] target(s) in 0.68s + Running unittests src/lib.rs (target/debug/deps/disktree_core-c3cb863045931ea5) + +running 1 test +test removal::tests::removal_nesting_budget ... plan M=128 work=9187 wall=11.07275ms +plan M=1024 work=537027 wall=398.130709ms + +thread 'removal::tests::removal_nesting_budget' (403560362) panicked at crates/disktree-core/src/removal.rs:1789:9: +8x marks: 9187 -> 537027 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED + +failures: + +failures: + removal::tests::removal_nesting_budget + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 149 filtered out; finished in 0.41s + +error: test failed, to rerun pass `-p disktree-core --lib` +EXIT 101 + +REPEAT 2: disktree-app baseline 5a3c92e + Finished `test` profile [optimized + debuginfo] target(s) in 0.28s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 4 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=34048 wall=278.333µs +refresh M=1024 N=2048 R=4 work=2107392 wall=9.065291ms + +thread 'marks::tests::refresh_resolution_budget' (403560424) panicked at crates/disktree-app/src/marks.rs:208:9: +8x marks/folders: 34048 -> 2107392 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED +test tests::marked_window_resolution_budget ... window M=128 N=256 draws=4 prepares=8 work=68312 wall=161.706417ms +window M=1024 N=2048 draws=4 prepares=8 work=4215000 wall=6.607910459s + +thread 'tests::marked_window_resolution_budget' (403560426) panicked at crates/disktree-app/src/tests.rs:1904:5: +8x marks/folders per frame: 68312/8 -> 4215000/8 +FAILED + +failures: + +failures: + marks::tests::refresh_resolution_budget + tests::marked_window_resolution_budget + +test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 69 filtered out; finished in 6.83s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` +EXIT 101 + +REPEAT 2: disktree-core baseline 5a3c92e + Finished `test` profile [optimized + debuginfo] target(s) in 0.09s + Running unittests src/lib.rs (target/debug/deps/disktree_core-c3cb863045931ea5) + +running 1 test +test removal::tests::removal_nesting_budget ... plan M=128 work=9187 wall=8.688291ms +plan M=1024 work=537027 wall=399.84275ms + +thread 'removal::tests::removal_nesting_budget' (403560664) panicked at crates/disktree-core/src/removal.rs:1789:9: +8x marks: 9187 -> 537027 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED + +failures: + +failures: + removal::tests::removal_nesting_budget + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 149 filtered out; finished in 0.41s + +error: test failed, to rerun pass `-p disktree-core --lib` +EXIT 101 + +REPEAT 3: disktree-app baseline 5a3c92e + Finished `test` profile [optimized + debuginfo] target(s) in 0.26s +warning: the following packages contain code that will be rejected by a future version of Rust: block v0.1.6 +note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1` + Running unittests src/main.rs (target/debug/deps/disktree-be72e126ba0d25ca) + +running 4 tests +test argument_tests::adaptive_budget_and_fixed_override_are_explicit ... ok +test argument_tests::invalid_worker_budgets_are_rejected ... ok +test marks::tests::refresh_resolution_budget ... refresh M=128 N=256 R=4 work=34048 wall=263.917µs +refresh M=1024 N=2048 R=4 work=2107392 wall=7.863542ms + +thread 'marks::tests::refresh_resolution_budget' (403560714) panicked at crates/disktree-app/src/marks.rs:208:9: +8x marks/folders: 34048 -> 2107392 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED +test tests::marked_window_resolution_budget ... window M=128 N=256 draws=4 prepares=8 work=68312 wall=155.442209ms +window M=1024 N=2048 draws=4 prepares=8 work=4215000 wall=6.564761291s + +thread 'tests::marked_window_resolution_budget' (403560715) panicked at crates/disktree-app/src/tests.rs:1904:5: +8x marks/folders per frame: 68312/8 -> 4215000/8 +FAILED + +failures: + +failures: + marks::tests::refresh_resolution_budget + tests::marked_window_resolution_budget + +test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 69 filtered out; finished in 6.77s + +error: test failed, to rerun pass `-p disktree-app --bin disktree` +EXIT 101 + +REPEAT 3: disktree-core baseline 5a3c92e + Finished `test` profile [optimized + debuginfo] target(s) in 0.10s + Running unittests src/lib.rs (target/debug/deps/disktree_core-c3cb863045931ea5) + +running 1 test +test removal::tests::removal_nesting_budget ... plan M=128 work=9187 wall=8.564791ms +plan M=1024 work=537027 wall=393.824917ms + +thread 'removal::tests::removal_nesting_budget' (403560958) panicked at crates/disktree-core/src/removal.rs:1789:9: +8x marks: 9187 -> 537027 +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +FAILED + +failures: + +failures: + removal::tests::removal_nesting_budget + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 149 filtered out; finished in 0.40s + +error: test failed, to rerun pass `-p disktree-core --lib` +EXIT 101