Skip to main content

max / audiofiles

Stop deep-cloning the collections and tag lists every frame collections was a Vec<Collection> deep-cloned in the sidebar and both context menus each frame. Make it Arc<Vec<Collection>> (matching nav.vfs_list and all_tags) -- it is only ever replaced wholesale by refresh_collections, so the snapshot-to-decouple-the-borrow clone is now a refcount bump. The sidebar tag list took the same treatment: the no-filter path now shares the all_tags Arc instead of copying the whole Vec<String>. The classifier draw_rules loop already uses mem::take + deferred actions, and nav.vfs_list was already an Arc, so both were left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 20:43 UTC
Commit: 72be856ca3d66047f1938d28540d9950d04d30da
Parent: 5e3fa8d
6 files changed, +86 insertions, -19 deletions
M Cargo.lock +59 -1
@@ -1168,6 +1168,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1168 1168 checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
1169 1169
1170 1170 [[package]]
1171 + name = "curve25519-dalek"
1172 + version = "4.1.3"
1173 + source = "registry+https://github.com/rust-lang/crates.io-index"
1174 + checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
1175 + dependencies = [
1176 + "cfg-if",
1177 + "cpufeatures 0.2.17",
1178 + "curve25519-dalek-derive",
1179 + "fiat-crypto",
1180 + "rustc_version",
1181 + "subtle",
1182 + "zeroize",
1183 + ]
1184 +
1185 + [[package]]
1186 + name = "curve25519-dalek-derive"
1187 + version = "0.1.1"
1188 + source = "registry+https://github.com/rust-lang/crates.io-index"
1189 + checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
1190 + dependencies = [
1191 + "proc-macro2",
1192 + "quote",
1193 + "syn 2.0.119",
1194 + ]
1195 +
1196 + [[package]]
1171 1197 name = "dasp_sample"
1172 1198 version = "0.11.0"
1173 1199 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1609,6 +1635,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1609 1635 checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
1610 1636
1611 1637 [[package]]
1638 + name = "fiat-crypto"
1639 + version = "0.2.9"
1640 + source = "registry+https://github.com/rust-lang/crates.io-index"
1641 + checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
1642 +
1643 + [[package]]
1612 1644 name = "field-offset"
1613 1645 version = "0.3.6"
1614 1646 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4455,7 +4487,7 @@ dependencies = [
4455 4487 "security-framework",
4456 4488 "security-framework-sys",
4457 4489 "webpki-root-certs",
4458 - "windows-sys 0.52.0",
4490 + "windows-sys 0.61.2",
4459 4491 ]
4460 4492
4461 4493 [[package]]
@@ -5338,6 +5370,7 @@ dependencies = [
5338 5370 "unicode-normalization",
5339 5371 "urlencoding",
5340 5372 "uuid",
5373 + "x25519-dalek",
5341 5374 "zeroize",
5342 5375 ]
5343 5376
@@ -6936,6 +6969,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
6936 6969 checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
6937 6970
6938 6971 [[package]]
6972 + name = "x25519-dalek"
6973 + version = "2.0.1"
6974 + source = "registry+https://github.com/rust-lang/crates.io-index"
6975 + checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
6976 + dependencies = [
6977 + "curve25519-dalek",
6978 + "rand_core 0.6.4",
6979 + "zeroize",
6980 + ]
6981 +
6982 + [[package]]
6939 6983 name = "xcursor"
6940 6984 version = "0.3.10"
6941 6985 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7107,6 +7151,20 @@ name = "zeroize"
7107 7151 version = "1.9.0"
7108 7152 source = "registry+https://github.com/rust-lang/crates.io-index"
7109 7153 checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
7154 + dependencies = [
7155 + "zeroize_derive",
7156 + ]
7157 +
7158 + [[package]]
7159 + name = "zeroize_derive"
7160 + version = "1.5.0"
7161 + source = "registry+https://github.com/rust-lang/crates.io-index"
7162 + checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
7163 + dependencies = [
7164 + "proc-macro2",
7165 + "quote",
7166 + "syn 2.0.119",
7167 + ]
7110 7168
7111 7169 [[package]]
7112 7170 name = "zerotrie"
@@ -267,10 +267,11 @@ impl BrowserState {
267 267
268 268 /// Refresh the collection list from the database.
269 269 pub fn refresh_collections(&mut self) {
270 - self.collections_ui.collections = self.backend.list_collections().unwrap_or_else(|e| {
271 - warn!("Failed to load collections: {e}");
272 - Vec::new()
273 - });
270 + self.collections_ui.collections =
271 + Arc::new(self.backend.list_collections().unwrap_or_else(|e| {
272 + warn!("Failed to load collections: {e}");
273 + Vec::new()
274 + }));
274 275 }
275 276
276 277 /// Activate a collection: show its members in the file list.
@@ -503,7 +503,7 @@ impl BrowserState {
503 503 last_dismissed_suggestion: None,
504 504 current_theme_id: theme_id,
505 505 collections_ui: CollectionsUiState {
506 - collections: collections_list,
506 + collections: Arc::new(collections_list),
507 507 ..Default::default()
508 508 },
509 509 edit: EditUiState::default(),
@@ -528,7 +528,10 @@ pub struct MirrorUiState {
528 528 /// Collections sidebar: list, active selection, and create/rename inputs.
529 529 #[derive(Default)]
530 530 pub struct CollectionsUiState {
531 - pub collections: Vec<audiofiles_core::collections::Collection>,
531 + // Arc so the sidebar / context menus can cheaply snapshot the list to
532 + // decouple the borrow (matching nav.vfs_list and all_tags) instead of
533 + // deep-cloning the Vec every frame. Replaced wholesale by refresh_collections.
534 + pub collections: std::sync::Arc<Vec<audiofiles_core::collections::Collection>>,
532 535 pub active_collection: Option<audiofiles_core::CollectionId>,
533 536 pub collection_create_input: String,
534 537 pub collection_rename_target: Option<(audiofiles_core::CollectionId, String)>,
@@ -114,7 +114,7 @@ pub fn draw_context_menu(
114 114 let is_in_collection = state.collections_ui.active_collection.is_some();
115 115 if !collections.is_empty() {
116 116 ui.menu_button("Add to Collection", |ui| {
117 - for coll in &collections {
117 + for coll in collections.iter() {
118 118 if ui.button(&coll.name).clicked() {
119 119 let _ = state.backend.add_to_collection(coll.id, &hash_clone);
120 120 state.refresh_collections();
@@ -262,7 +262,7 @@ pub fn draw_multi_context_menu(ui: &mut egui::Ui, state: &mut BrowserState) {
262 262 let collections = state.collections_ui.collections.clone();
263 263 if !collections.is_empty() {
264 264 ui.menu_button("Add to Collection", |ui| {
265 - for coll in &collections {
265 + for coll in collections.iter() {
266 266 if ui.button(&coll.name).clicked() {
267 267 let nodes = state.selected_nodes();
268 268 // Count only samples actually added — directories (no hash)
@@ -371,7 +371,7 @@ pub fn draw_sidebar(ui: &mut egui::Ui, state: &mut BrowserState) {
371 371 let collections = state.collections_ui.collections.clone();
372 372 let active_id = state.collections_ui.active_collection;
373 373 let mut delete_id: Option<(audiofiles_core::CollectionId, String)> = None;
374 - for coll in &collections {
374 + for coll in collections.iter() {
375 375 let is_active = active_id == Some(coll.id);
376 376 // Dynamic collections re-apply their saved filter; manual collections
377 377 // hold a fixed sample set. Distinguish with a text suffix instead of a
@@ -529,15 +529,20 @@ pub fn draw_sidebar(ui: &mut egui::Ui, state: &mut BrowserState) {
529 529 // render alongside the filter input.
530 530 let total = state.all_tags.len();
531 531 let query = state.search.tag_search.to_lowercase();
532 - let filtered_tags: Vec<String> = if query.is_empty() {
533 - state.all_tags.as_ref().clone()
532 + // No filter is the common case: share the Arc rather than deep-cloning
533 + // the whole tag list every frame. A filter still materializes the
534 + // (smaller) matching subset.
535 + let filtered_tags: std::sync::Arc<Vec<String>> = if query.is_empty() {
536 + std::sync::Arc::clone(&state.all_tags)
534 537 } else {
535 - state
536 - .all_tags
537 - .iter()
538 - .filter(|t| t.to_lowercase().contains(&query))
539 - .cloned()
540 - .collect()
538 + std::sync::Arc::new(
539 + state
540 + .all_tags
541 + .iter()
542 + .filter(|t| t.to_lowercase().contains(&query))
543 + .cloned()
544 + .collect(),
545 + )
541 546 };
542 547 // Tag filter input — pair with a Clear button when populated so the
543 548 // user doesn't have to select-all-and-delete. Mirrors the sample
@@ -657,7 +662,7 @@ pub fn draw_sidebar(ui: &mut egui::Ui, state: &mut BrowserState) {
657 662 if filtered_tags.is_empty() {
658 663 ui.label(egui::RichText::new("No matching tags").color(theme::content_muted()));
659 664 } else {
660 - let tree = build_tag_tree(&filtered_tags);
665 + let tree = build_tag_tree(filtered_tags.as_slice());
661 666 for (segment, node) in &tree {
662 667 draw_tag_node(ui, "", segment, node, state);
663 668 }