mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
fix: optimize serve/sourcemap startup from >180s to ~8s
Root cause: notify-debouncer-full with macos_kqueue caused 25K+ directory watch registrations to take >170s on large projects. Changes: - Replace notify-debouncer-full with notify-debouncer-mini to avoid FSEvents Create+Remove event cancellation and heavy per-watch overhead - Remove macos_kqueue feature; use FSEvents (matches upstream Rojo) - Fix VFS read guard: watch_enabled && watch_recursive -> watch_enabled so recording mechanism works when watch_recursive is false - Build RefPathIndex from patch_apply data instead of redundant 4.8s WalkDir + 5,854 file re-reads (populate_from_dir) - Use ServeSession::new_oneshot() for sourcemap without --watch - Restore simple synchronous watch setup (implicit during reads) Serve: >180s (never completed) -> ~8s to Listening Sourcemap: 14.3s -> ~9s All 489 tests passing.
This commit is contained in:
parent
4ddc4a3e31
commit
9cf4cea936
10 changed files with 236 additions and 210 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<HashSet<PathBuf>>, Vec<PathBuf>), ServeSessionError>
|
||||
{
|
||||
) -> Result<
|
||||
(
|
||||
Project,
|
||||
RojoTree,
|
||||
Option<HashSet<PathBuf>>,
|
||||
Vec<PathBuf>,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<Ref>,
|
||||
pub added: Vec<Ref>,
|
||||
pub updated: Vec<AppliedPatchUpdate>,
|
||||
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Ref>,
|
||||
|
||||
/// 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<PathBuf> {
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue