diff --git a/CHANGELOG.md b/CHANGELOG.md index 020171c6..c5513848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ Making a new release? Simply add the new header with the version and date undern ## Unreleased +* Add .gitignore for logs and refactor ServeSession to include path_roots in init_tree return + ## [8.5.6] (March 8th, 2026) * Add asynchronous log file compression with lowered thread priority to avoid impacting main workloads diff --git a/Cargo.lock b/Cargo.lock index ebf435e3..f97344fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1030,15 +1030,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "file-id" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "filetime" version = "0.2.27" @@ -1920,7 +1911,7 @@ dependencies = [ "fs-err", "log", "notify", - "notify-debouncer-full", + "notify-debouncer-mini", "serde", "tempfile", ] @@ -1991,16 +1982,15 @@ dependencies = [ ] [[package]] -name = "notify-debouncer-full" +name = "notify-debouncer-mini" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302" +checksum = "17849edfaabd9a5fef1c606d99cfc615a8e99f7ac4366406d86c7942a3184cf2" dependencies = [ - "file-id", "log", "notify", "notify-types", - "walkdir", + "tempfile", ] [[package]] diff --git a/crates/memofs/Cargo.toml b/crates/memofs/Cargo.toml index 1eb76ec5..7409e80f 100644 --- a/crates/memofs/Cargo.toml +++ b/crates/memofs/Cargo.toml @@ -18,14 +18,9 @@ homepage = "https://github.com/UserGeneratedLLC/rojo/tree/master/memofs" crossbeam-channel = "0.5.15" fs-err = "3" log = "0.4" -notify-debouncer-full = "0.7" -serde = { version = "1.0.228", features = ["derive"] } - -[target.'cfg(target_os = "macos")'.dependencies] -notify = { version = "8.2", features = ["macos_kqueue"] } - -[target.'cfg(not(target_os = "macos"))'.dependencies] notify = "8.2" +notify-debouncer-mini = "0.7" +serde = { version = "1.0.228", features = ["derive"] } [dev-dependencies] tempfile = "3.26.0" diff --git a/crates/memofs/src/lib.rs b/crates/memofs/src/lib.rs index c99acb1a..6a825de5 100644 --- a/crates/memofs/src/lib.rs +++ b/crates/memofs/src/lib.rs @@ -190,7 +190,7 @@ impl VfsInner { fn read_raw(&mut self, path: &Path) -> io::Result> { if let Some(cache) = &mut self.prefetch_cache { if let Some(contents) = cache.files.remove(path) { - if self.watch_enabled && self.watch_recursive { + if self.watch_enabled { self.watch_or_record(path)?; } return Ok(contents); @@ -199,7 +199,7 @@ impl VfsInner { let contents = self.backend.read(path)?; - if self.watch_enabled && self.watch_recursive { + if self.watch_enabled { self.watch_or_record(path)?; } diff --git a/crates/memofs/src/std_backend.rs b/crates/memofs/src/std_backend.rs index bf44ca28..0a669481 100644 --- a/crates/memofs/src/std_backend.rs +++ b/crates/memofs/src/std_backend.rs @@ -4,11 +4,7 @@ use std::{collections::HashSet, io}; use crossbeam_channel::{Receiver, Sender}; use notify::RecursiveMode; -use notify_debouncer_full::{ - new_debouncer, - notify::event::{CreateKind, EventKind, ModifyKind, RemoveKind, RenameMode}, - DebounceEventResult, Debouncer, RecommendedCache, -}; +use notify_debouncer_mini::{new_debouncer, DebounceEventResult, DebouncedEventKind, Debouncer}; use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent}; @@ -55,7 +51,7 @@ pub type CriticalErrorHandler = Box bool + Send /// `VfsBackend` that uses `std::fs` and the `notify` crate. pub struct StdBackend { - debouncer: Debouncer, + debouncer: Debouncer, watcher_receiver: Receiver, watches: HashSet, recursive_watches: HashSet, @@ -123,136 +119,48 @@ impl StdBackend { event_tx: Sender, error_tx: Sender, error_handler: CriticalErrorHandler, - ) -> Debouncer { - // Use 50ms debounce timeout (same as the old v4 implementation) + ) -> Debouncer { let debounce_timeout = Duration::from_millis(50); new_debouncer( debounce_timeout, - None, // Use default tick rate - move |result: DebounceEventResult| { - match result { - Ok(events) => { - for event in events { - let vfs_events = Self::convert_event(&event.event); - for vfs_event in vfs_events { - if let Err(err) = event_tx.send(vfs_event) { - let critical_err = - WatcherCriticalError::ChannelSendFailed(err.to_string()); - let _ = error_tx.send(critical_err.clone()); - if error_handler(critical_err) { - return; - } - } - } - } - } - Err(errors) => { - for error in errors { - // Check if this is a rescan request - if error.paths.is_empty() { - let critical_err = WatcherCriticalError::RescanRequired; - let _ = error_tx.send(critical_err.clone()); - if error_handler(critical_err) { - return; - } - } else { - let critical_err = WatcherCriticalError::WatcherError { - error: format!("{:?}", error.kind), - path: error.paths.first().cloned(), - }; - let _ = error_tx.send(critical_err.clone()); - if error_handler(critical_err) { - return; + move |result: DebounceEventResult| match result { + Ok(events) => { + for event in events { + let vfs_event = match event.kind { + DebouncedEventKind::Any | DebouncedEventKind::AnyContinuous => { + if event.path.exists() { + VfsEvent::Write(event.path) + } else { + VfsEvent::Remove(event.path) } } + _ => continue, + }; + log::trace!("VFS debounced event: {:?}", &vfs_event); + if let Err(err) = event_tx.send(vfs_event) { + let critical_err = + WatcherCriticalError::ChannelSendFailed(err.to_string()); + let _ = error_tx.send(critical_err.clone()); + if error_handler(critical_err) { + return; + } } } } + Err(error) => { + let critical_err = WatcherCriticalError::WatcherError { + error: format!("{:?}", error.kind), + path: error.paths.first().cloned(), + }; + let _ = error_tx.send(critical_err.clone()); + error_handler(critical_err); + } }, ) .expect("Failed to create file watcher debouncer") } - /// Convert a notify event to our VfsEvent(s) - fn convert_event(event: ¬ify::Event) -> Vec { - let mut vfs_events = Vec::new(); - - match &event.kind { - // Create events - EventKind::Create(CreateKind::File) - | EventKind::Create(CreateKind::Folder) - | EventKind::Create(CreateKind::Any) - | EventKind::Create(CreateKind::Other) => { - for path in &event.paths { - vfs_events.push(VfsEvent::Create(path.clone())); - } - } - - // Modify events (treat as Write) - EventKind::Modify(ModifyKind::Data(_)) - | EventKind::Modify(ModifyKind::Any) - | EventKind::Modify(ModifyKind::Other) => { - for path in &event.paths { - vfs_events.push(VfsEvent::Write(path.clone())); - } - } - - // Metadata changes - we don't care about these for Rojo's purposes - EventKind::Modify(ModifyKind::Metadata(_)) => {} - - // Name changes (renames) - the debouncer-full handles rename tracking - EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { - // Both paths present: old path at [0], new path at [1] - if event.paths.len() >= 2 { - vfs_events.push(VfsEvent::Remove(event.paths[0].clone())); - vfs_events.push(VfsEvent::Create(event.paths[1].clone())); - } - } - EventKind::Modify(ModifyKind::Name(RenameMode::From)) => { - // Only the old path - treat as removal - for path in &event.paths { - vfs_events.push(VfsEvent::Remove(path.clone())); - } - } - EventKind::Modify(ModifyKind::Name(RenameMode::To)) => { - // Only the new path - treat as creation - for path in &event.paths { - vfs_events.push(VfsEvent::Create(path.clone())); - } - } - EventKind::Modify(ModifyKind::Name(RenameMode::Any)) - | EventKind::Modify(ModifyKind::Name(RenameMode::Other)) => { - // Ambiguous rename - treat as modification - for path in &event.paths { - vfs_events.push(VfsEvent::Write(path.clone())); - } - } - - // Remove events - EventKind::Remove(RemoveKind::File) - | EventKind::Remove(RemoveKind::Folder) - | EventKind::Remove(RemoveKind::Any) - | EventKind::Remove(RemoveKind::Other) => { - for path in &event.paths { - vfs_events.push(VfsEvent::Remove(path.clone())); - } - } - - // Access events - we don't care about these - EventKind::Access(_) => {} - - // Other/Any events - treat as potential modifications - EventKind::Other | EventKind::Any => { - for path in &event.paths { - vfs_events.push(VfsEvent::Write(path.clone())); - } - } - } - - vfs_events - } - /// Returns a receiver for critical errors from the watcher thread. /// /// Poll this alongside `event_receiver()` to detect when watching becomes unreliable. @@ -347,7 +255,7 @@ impl VfsBackend for StdBackend { } else { RecursiveMode::NonRecursive }; - match self.debouncer.watch(path, mode) { + match self.debouncer.watcher().watch(path, mode) { Ok(()) => { log::debug!( "Watching path ({}): {}", @@ -374,7 +282,7 @@ impl VfsBackend for StdBackend { fn unwatch(&mut self, path: &Path) -> io::Result<()> { let was_watched = self.watches.contains(path); - match self.debouncer.unwatch(path) { + match self.debouncer.watcher().unwatch(path) { Ok(()) => { if was_watched { log::info!("Unwatched path: {}", path.display()); diff --git a/src/cli/sourcemap.rs b/src/cli/sourcemap.rs index 64143b06..a5588963 100644 --- a/src/cli/sourcemap.rs +++ b/src/cli/sourcemap.rs @@ -79,7 +79,17 @@ impl SourcemapCommand { let vfs = Vfs::new_default(); vfs.set_watch_enabled(self.watch); - let session = ServeSession::new(vfs, project_path, None)?; + let session_start = std::time::Instant::now(); + let session = if self.watch { + ServeSession::new(vfs, project_path, None)? + } else { + ServeSession::new_oneshot(vfs, project_path)? + }; + log::debug!( + "[PERF] ServeSession::new() total: {:.1?}", + session_start.elapsed() + ); + let mut cursor = session.message_queue().cursor(); let filter = if self.include_non_scripts { @@ -93,6 +103,7 @@ impl SourcemapCommand { .build_global() .ok(); + let sm_start = std::time::Instant::now(); write_sourcemap( &session, self.output.as_deref(), @@ -100,6 +111,7 @@ impl SourcemapCommand { self.absolute, false, )?; + log::debug!("[PERF] write_sourcemap: {:.1?}", sm_start.elapsed()); if self.watch { if !std::io::stdin().is_terminal() { @@ -259,6 +271,7 @@ pub(crate) fn write_sourcemap( use_absolute_paths: bool, quiet: bool, ) -> anyhow::Result<()> { + let t0 = std::time::Instant::now(); let tree = session.tree(); let canonical_project_dir = session.root_dir().to_path_buf(); @@ -270,22 +283,32 @@ pub(crate) fn write_sourcemap( filter, use_absolute_paths, ); + let t1 = std::time::Instant::now(); if let Some(output_path) = output { - // Use standard JSON (not JSON5) for sourcemaps - required by external tools like LSPs let json_output = serde_json::to_string(&root_node)?; + let t2 = std::time::Instant::now(); - // Use atomic write (temp file + rename) to prevent file watchers from - // reading partial files. Rename is atomic on all major filesystems when - // source and destination are on the same filesystem. write_atomic(output_path, json_output.as_bytes())?; + let t3 = std::time::Instant::now(); + + log::debug!( + "[PERF] write_sourcemap: tree_walk={:.1?}, json_serialize={:.1?} ({} bytes), write={:.1?}", + t1 - t0, + t2 - t1, + json_output.len(), + t3 - t2, + ); if !quiet { println!("Created sourcemap at {}", output_path.display()); } } else { - // Use standard JSON (not JSON5) for sourcemaps - required by external tools like LSPs let output = serde_json::to_string(&root_node)?; + log::debug!( + "[PERF] write_sourcemap: tree_walk={:.1?}, json_serialize=inline", + t1 - t0 + ); println!("{}", output); } diff --git a/src/rojo_ref.rs b/src/rojo_ref.rs index dc6ac271..6783a541 100644 --- a/src/rojo_ref.rs +++ b/src/rojo_ref.rs @@ -326,6 +326,22 @@ impl RefPathIndex { } } + /// Build from pre-collected entries (avoids re-walking the directory tree). + pub fn from_entries(entries: Vec<(String, PathBuf)>) -> Self { + let mut index = Self::new(); + let count = entries.len(); + for (ref_path, meta_file) in entries { + index.add(&ref_path, &meta_file); + } + if count > 0 { + log::debug!( + "RefPathIndex: built from {} collected Rojo_Ref_* entries", + count + ); + } + index + } + /// Record that `meta_file` contains a `Rojo_Ref_*` attribute with value /// `ref_path` (the instance path string, e.g., "Workspace/Model/Part1"). pub fn add(&mut self, ref_path: &str, meta_file: &Path) { diff --git a/src/serve_session.rs b/src/serve_session.rs index b69f6cc9..48549fe2 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -326,12 +326,22 @@ pub fn collect_path_roots(node: &crate::project::ProjectNode, base: &Path, out: impl ServeSession { /// Shared initialization: loads the project and builds the initial /// snapshot tree. Used by both `new()` and `new_oneshot()`. - /// Returns the walked paths from prefetch (if available) for syncback reuse. + /// Returns the walked paths from prefetch (if available) for syncback reuse, + /// plus collected ref path index entries for building RefPathIndex. + #[allow(clippy::type_complexity)] fn init_tree( vfs: &Vfs, start_path: &Path, - ) -> Result<(Project, RojoTree, Option>, Vec), ServeSessionError> - { + ) -> Result< + ( + Project, + RojoTree, + Option>, + Vec, + Vec<(String, std::path::PathBuf)>, + ), + ServeSessionError, + > { log::trace!("Starting new ServeSession at path {}", start_path.display()); let root_project = Project::load_initial_project(vfs, start_path)?; @@ -413,48 +423,27 @@ impl ServeSession { let snap_start = Instant::now(); log::trace!("Generating snapshot of instances from VFS"); - let was_watch_enabled = vfs.is_watch_enabled(); - vfs.set_watch_enabled(false); let snapshot = snapshot_from_vfs(&instance_context, vfs, start_path)?; - vfs.set_watch_enabled(was_watch_enabled); log::debug!("Snapshot built in {:.1?}", snap_start.elapsed()); vfs.clear_prefetch_cache(); let patch_start = Instant::now(); + log::trace!("Computing initial patch set"); + let patch_set = compute_patch_set(snapshot, &tree, root_id); - if vfs.is_watch_enabled() { - std::thread::scope(|s| { - s.spawn(|| { - let watch_start = Instant::now(); - let mut watch_count: usize = 0; - for root in &path_roots { - let _ = vfs.watch(root); - watch_count += 1; - } - log::debug!( - "Set up {} watches in {:.1?}", - watch_count, - watch_start.elapsed(), - ); - }); + log::trace!("Applying initial patch set"); + let applied = apply_patch_set(&mut tree, patch_set); + let ref_path_entries = applied.ref_path_index_entries; + log::debug!("Patch computed + applied in {:.1?}", patch_start.elapsed()); - log::trace!("Computing initial patch set"); - let patch_set = compute_patch_set(snapshot, &tree, root_id); - - log::trace!("Applying initial patch set"); - apply_patch_set(&mut tree, patch_set); - }); - } else { - log::trace!("Computing initial patch set"); - let patch_set = compute_patch_set(snapshot, &tree, root_id); - - log::trace!("Applying initial patch set"); - apply_patch_set(&mut tree, patch_set); - } - log::debug!("Watch + patch in {:.1?}", patch_start.elapsed()); - - Ok((root_project, tree, walked_paths, path_roots)) + Ok(( + root_project, + tree, + walked_paths, + path_roots, + ref_path_entries, + )) } /// Start a new serve session from the given in-memory filesystem and start @@ -471,7 +460,10 @@ impl ServeSession { let start_path = start_path.as_ref(); let start_time = Instant::now(); - let (root_project, tree, _walked_paths, _path_roots) = Self::init_tree(&vfs, start_path)?; + let t_init_start = Instant::now(); + let (root_project, tree, _walked_paths, _path_roots, ref_path_entries) = + Self::init_tree(&vfs, start_path)?; + let t_init_tree = Instant::now(); let session_id = SessionId::new(); let message_queue = MessageQueue::new(); @@ -482,29 +474,26 @@ impl ServeSession { let (tree_mutation_sender, tree_mutation_receiver) = crossbeam_channel::unbounded(); let suppressed_paths = Arc::new(Mutex::new(std::collections::HashMap::new())); - let ref_path_index = { - let mut index = crate::RefPathIndex::new(); - let tree_guard = tree.lock().unwrap(); - let mut ref_roots = Vec::new(); - collect_path_roots( - &root_project.tree, - root_project.folder_location(), - &mut ref_roots, - ); - for root in &ref_roots { - index.populate_from_dir(root, &tree_guard); - } - drop(tree_guard); - Arc::new(Mutex::new(index)) - }; + let ref_path_index = Arc::new(Mutex::new(crate::RefPathIndex::from_entries( + ref_path_entries, + ))); + let t_ref_index = Instant::now(); let git_repo_root = crate::git::git_repo_root(root_project.folder_location()); let initial_head_commit = git_repo_root .as_deref() .and_then(crate::git::git_head_commit); + let t_git = Instant::now(); let path_ignore_rules = root_project.path_ignore_rules(); + log::debug!( + "[PERF] ServeSession::new breakdown: init_tree={:.1?}, ref_path_index={:.1?}, git={:.1?}", + t_init_tree - t_init_start, + t_ref_index - t_init_tree, + t_git - t_ref_index, + ); + log::trace!("Starting ChangeProcessor"); let change_processor = ChangeProcessor::start( Arc::clone(&tree), @@ -547,7 +536,8 @@ impl ServeSession { let start_path = start_path.as_ref(); let start_time = Instant::now(); - let (root_project, tree, walked_paths, _path_roots) = Self::init_tree(&vfs, start_path)?; + let (root_project, tree, walked_paths, _path_roots, _ref_entries) = + Self::init_tree(&vfs, start_path)?; Ok(Self { change_processor: None, diff --git a/src/snapshot/patch.rs b/src/snapshot/patch.rs index 235ed12d..d3551e42 100644 --- a/src/snapshot/patch.rs +++ b/src/snapshot/patch.rs @@ -1,6 +1,6 @@ //! Defines the data structures used for describing instance patches. -use std::collections::HashSet; +use std::{collections::HashSet, path::PathBuf}; use rbx_dom_weak::{ types::{Ref, Variant}, @@ -81,6 +81,11 @@ pub struct AppliedPatchSet { pub removed: Vec, pub added: Vec, pub updated: Vec, + + /// Collected during patch application: `(resolved_absolute_ref_path, meta_or_model_file_path)`. + /// Used to build `RefPathIndex` without re-walking the directory tree. + #[serde(skip)] + pub ref_path_index_entries: Vec<(String, PathBuf)>, } impl AppliedPatchSet { @@ -89,6 +94,7 @@ impl AppliedPatchSet { removed: Vec::new(), added: Vec::new(), updated: Vec::new(), + ref_path_index_entries: Vec::new(), } } diff --git a/src/snapshot/patch_apply.rs b/src/snapshot/patch_apply.rs index 41830f49..5814c7be 100644 --- a/src/snapshot/patch_apply.rs +++ b/src/snapshot/patch_apply.rs @@ -3,6 +3,8 @@ use std::{ collections::{HashMap, HashSet}, mem::take, + path::PathBuf, + time::Instant, }; use rbx_dom_weak::{ @@ -24,14 +26,20 @@ use crate::{ /// tree in sync with Rojo's. #[profiling::function] pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatchSet { + let t0 = Instant::now(); let mut context = PatchApplyContext::default(); + let removal_count = patch_set.removed_instances.len(); + let addition_count = patch_set.added_instances.len(); + let update_count = patch_set.updated_instances.len(); + { profiling::scope!("removals"); for removed_id in patch_set.removed_instances { apply_remove_instance(&mut context, tree, removed_id); } } + let t1 = Instant::now(); { profiling::scope!("additions"); @@ -39,6 +47,7 @@ pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatch apply_add_child(&mut context, tree, add_patch.parent_id, add_patch.instance); } } + let t2 = Instant::now(); { profiling::scope!("updates"); @@ -46,6 +55,22 @@ pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatch apply_update_child(&mut context, tree, update_patch); } } + let t3 = Instant::now(); + + log::debug!( + "[PERF] patch_apply phases: removals={} in {:.1?}, additions={} in {:.1?}, updates={} in {:.1?}", + removal_count, t1 - t0, + addition_count, t2 - t1, + update_count, t3 - t2, + ); + log::debug!( + "[PERF] patch_apply context: snapshot_id_map={}, has_refs={}, attr_refs={}, path_refs={}, cleanup={}", + context.snapshot_id_to_instance_id.len(), + context.has_refs_to_rewrite.len(), + context.attribute_refs_to_rewrite.keys().count(), + context.path_refs_to_rewrite.keys().count(), + context.instances_needing_attr_cleanup.len(), + ); finalize_patch_application(context, tree) } @@ -90,6 +115,10 @@ struct PatchApplyContext { /// format and should not persist in the live game. instances_needing_attr_cleanup: HashSet, + /// Collected `(resolved_absolute_ref_path, meta_or_model_file_path)` entries + /// for building `RefPathIndex` without a redundant directory walk. + ref_path_index_entries: Vec<(String, PathBuf)>, + /// The current applied patch result, describing changes made to the tree. applied_patch_set: AppliedPatchSet, } @@ -105,7 +134,12 @@ struct PatchApplyContext { /// where we build up a map of snapshot IDs to instance IDs as they're created, /// then apply properties all at once at the end. #[profiling::function] -fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -> AppliedPatchSet { +fn finalize_patch_application( + mut context: PatchApplyContext, + tree: &mut RojoTree, +) -> AppliedPatchSet { + let finalize_start = Instant::now(); + for id in context.has_refs_to_rewrite { let mut instance = tree .get_instance_mut(id) @@ -119,11 +153,13 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) - } } } + let t_refs_rewrite = Instant::now(); // IMPORTANT: Rojo_Target_* (legacy) must be resolved BEFORE Rojo_Ref_* // (path-based) so that path-based results overwrite legacy results when // both exist for the same property. Do not reorder these two blocks. let mut real_rewrites = Vec::new(); + let attr_ref_count = context.attribute_refs_to_rewrite.keys().count(); for (id, map) in context.attribute_refs_to_rewrite { for (prop_name, prop_value) in map { if let Some(target) = tree.get_specified_id(&RojoRef::new(prop_value)) { @@ -135,11 +171,29 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) - .expect("Invalid instance ID in deferred attribute ref map"); instance.properties_mut().extend(real_rewrites.drain(..)); } + let t_attr_refs = Instant::now(); // Handle path-based references (Rojo_Ref_) let mut path_rewrites = Vec::new(); + let _path_ref_count = context.path_refs_to_rewrite.keys().count(); + let mut path_ref_total = 0usize; + let mut path_ref_resolved = 0usize; + let mut path_ref_failed = 0usize; for (id, map) in context.path_refs_to_rewrite { + let source_abs = crate::ref_target_path_from_tree(tree, id); + let meta_file = find_meta_or_model_path(tree, id); + for (prop_name, path) in map { + path_ref_total += 1; + + if let Some(meta_path) = &meta_file { + let resolved = crate::resolve_ref_path_to_absolute(&path, &source_abs) + .unwrap_or_else(|| path.clone()); + context + .ref_path_index_entries + .push((resolved, meta_path.clone())); + } + if let Some(target) = tree.resolve_ref_path(&path, id) { log::trace!( "Resolved path reference {} -> {} (path: {})", @@ -147,8 +201,10 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) - target, path ); + path_ref_resolved += 1; path_rewrites.push((prop_name, Variant::Ref(target))); } else { + path_ref_failed += 1; log::debug!( "Could not resolve path reference {} at path '{}' - instance not found in tree", prop_name, @@ -163,9 +219,11 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) - instance.properties_mut().extend(path_rewrites.drain(..)); } } + let t_path_refs = Instant::now(); // Clean up Rojo ref attributes - they're only used for transport in the file // format and should not persist in the live game + let cleanup_count = context.instances_needing_attr_cleanup.len(); for id in context.instances_needing_attr_cleanup { let mut instance = match tree.get_instance_mut(id) { Some(inst) => inst, @@ -195,8 +253,25 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) - attributes.remove(key); } } + let t_cleanup = Instant::now(); - context.applied_patch_set + log::debug!( + "[PERF] finalize_patch_application: refs_rewrite={:.1?}, attr_refs({})={:.1?}, path_refs({}, ok={}, fail={})={:.1?}, cleanup({})={:.1?}, total={:.1?}", + t_refs_rewrite - finalize_start, + attr_ref_count, + t_attr_refs - t_refs_rewrite, + path_ref_total, + path_ref_resolved, + path_ref_failed, + t_path_refs - t_attr_refs, + cleanup_count, + t_cleanup - t_path_refs, + t_cleanup - finalize_start, + ); + + let mut result = context.applied_patch_set; + result.ref_path_index_entries = context.ref_path_index_entries; + result } fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: Ref) { @@ -319,6 +394,27 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc context.applied_patch_set.updated.push(applied_patch) } +fn is_meta_or_model(name: &str) -> bool { + name.ends_with(".meta.json5") + || name.ends_with(".model.json5") + || name.ends_with(".meta.json") + || name.ends_with(".model.json") +} + +fn find_meta_or_model_path(tree: &RojoTree, id: Ref) -> Option { + let instance = tree.get_instance(id)?; + instance + .metadata() + .relevant_paths + .iter() + .find(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(is_meta_or_model) + }) + .cloned() +} + /// Calculates manually-specified Ref properties and marks them in the provided /// `PatchApplyContext` to be rewritten at the end of the patch application /// process.