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