From 04f2ad06ce68a165578a8c73d2befe6ec5cd2096 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 26 Oct 2026 14:48:04 +0900 Subject: [PATCH 1/2] perf(session): share one GlobResolver across task graph loading and planning The session now owns the resolver and passes it to `IndexedTaskGraph::load`, `plan_query` and `plan_synthetic`, so a cache glob pattern that both a task config and a synthetic command use is parsed once per session. Co-Authored-By: Claude Opus 5.5 --- crates/vt/src/session/mod.rs | 29 +++++++++++++++++++++++------ crates/vt_graph/src/config/mod.rs | 2 +- crates/vt_graph/src/lib.rs | 6 +++--- crates/vt_plan/src/context.rs | 10 +++++++--- crates/vt_plan/src/lib.rs | 7 +++++-- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/crates/vt/src/session/mod.rs b/crates/vt/src/session/mod.rs index 5d449279b..f1441eebd 100644 --- a/crates/vt/src/session/mod.rs +++ b/crates/vt/src/session/mod.rs @@ -4,7 +4,7 @@ mod execute; pub(crate) mod reporter; // Re-export types that are part of the public API -use std::{ffi::OsStr, fmt::Debug, io::IsTerminal, sync::Arc}; +use std::{ffi::OsStr, fmt::Debug, io::IsTerminal, rc::Rc, sync::Arc}; use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; @@ -19,8 +19,10 @@ use reporter::{ use rustc_hash::FxHashMap; use vt_casefold::EnvName; use vt_graph::{ - IndexedTaskGraph, TaskGraph, TaskGraphLoadError, config::user::UserCacheConfig, - loader::UserConfigLoader, query::TaskQuery, + IndexedTaskGraph, TaskGraph, TaskGraphLoadError, + config::{GlobResolver, user::UserCacheConfig}, + loader::UserConfigLoader, + query::TaskQuery, }; use vt_path::{AbsolutePath, AbsolutePathBuf}; use vt_plan::{ @@ -57,7 +59,11 @@ where #[derive(Debug)] enum LazyTaskGraph<'a> { - Uninitialized { workspace_root: WorkspaceRoot, config_loader: &'a dyn UserConfigLoader }, + Uninitialized { + workspace_root: WorkspaceRoot, + config_loader: &'a dyn UserConfigLoader, + glob_resolver: Rc, + }, Initialized(IndexedTaskGraph), } @@ -66,8 +72,9 @@ impl TaskGraphLoader for LazyTaskGraph<'_> { async fn load_task_graph(&mut self) -> Result<&vt_graph::IndexedTaskGraph, TaskGraphLoadError> { let _span = tracing::debug_span!("load_task_graph").entered(); Ok(match self { - Self::Uninitialized { workspace_root, config_loader } => { - let graph = IndexedTaskGraph::load(workspace_root, *config_loader).await?; + Self::Uninitialized { workspace_root, config_loader, glob_resolver } => { + let graph = + IndexedTaskGraph::load(workspace_root, *config_loader, glob_resolver).await?; *self = Self::Initialized(graph); match self { Self::Initialized(graph) => &*graph, @@ -158,6 +165,9 @@ pub struct Session<'a> { /// The task graph is loaded on-demand and cached for future use. lazy_task_graph: LazyTaskGraph<'a>, + /// Shared by task graph loading and every plan, so each cache glob pattern is parsed once. + glob_resolver: Rc, + envs: Arc>, Arc>>, cwd: Arc, @@ -240,13 +250,17 @@ impl<'a> Session<'a> { let workspace_node_modules_bin = workspace_root.path.join("node_modules").join(".bin"); prepend_path_env(&mut envs, &workspace_node_modules_bin)?; + let glob_resolver = Rc::new(GlobResolver::default()); + // Cache is lazily initialized on first access to avoid SQLite race conditions Ok(Self { workspace_path: Arc::clone(&workspace_root.path), lazy_task_graph: LazyTaskGraph::Uninitialized { workspace_root, config_loader: config.user_config_loader, + glob_resolver: Rc::clone(&glob_resolver), }, + glob_resolver, envs: Arc::new(envs), cwd, plan_request_parser: PlanRequestParser { command_handler: config.command_handler }, @@ -713,6 +727,7 @@ impl<'a> Session<'a> { &self.cwd, synthetic_plan_request, cache_key, + &self.glob_resolver, )?; // Initialize cache (needed for cache-aware execution) @@ -808,6 +823,7 @@ impl<'a> Session<'a> { &self.envs, &mut self.plan_request_parser, &mut self.lazy_task_graph, + &self.glob_resolver, ) .await?; Ok((plan_result, is_cwd_only)) @@ -829,6 +845,7 @@ impl<'a> Session<'a> { &self.envs, &mut self.plan_request_parser, &mut self.lazy_task_graph, + &self.glob_resolver, ) .await?; Ok(plan_result.graph) diff --git a/crates/vt_graph/src/config/mod.rs b/crates/vt_graph/src/config/mod.rs index 1ef299541..3f2eecff3 100644 --- a/crates/vt_graph/src/config/mod.rs +++ b/crates/vt_graph/src/config/mod.rs @@ -288,7 +288,7 @@ impl ResolvedGlobConfig { /// workspaces repeat the same patterns across packages and tasks, so loading a task graph would /// otherwise compile the same expressions thousands of times. /// -/// Create one for each task graph load or plan and pass it by reference. It is neither `Send` nor +/// Pass one by reference to every resolve that should share the cache. It is neither `Send` nor /// `Sync`, so its cache needs no lock. #[derive(Debug, Default)] pub struct GlobResolver { diff --git a/crates/vt_graph/src/lib.rs b/crates/vt_graph/src/lib.rs index 9f4323bff..3c8265eaa 100644 --- a/crates/vt_graph/src/lib.rs +++ b/crates/vt_graph/src/lib.rs @@ -258,6 +258,7 @@ impl IndexedTaskGraph { pub async fn load( workspace_root: &WorkspaceRoot, config_loader: &dyn loader::UserConfigLoader, + glob_resolver: &GlobResolver, ) -> Result { let mut task_graph = DiGraph::::default(); @@ -319,7 +320,6 @@ impl IndexedTaskGraph { let resolved_global_cache = ResolvedGlobalCacheConfig::resolve_from(root_cache.as_ref()); let mut top_level_cache_fields = TopLevelCacheFieldsCollector::default(); - let glob_resolver = GlobResolver::default(); // Second pass: create task nodes (cache is NOT applied here; it's applied at plan time) for (package_index, package_dir, user_config) in package_configs { @@ -370,7 +370,7 @@ impl IndexedTaskGraph { task_user_config, &package_dir, &workspace_root.path, - &glob_resolver, + glob_resolver, ) .map_err(|err| TaskGraphLoadError::ResolveConfigError { error: err, @@ -404,7 +404,7 @@ impl IndexedTaskGraph { &package_dir, package_json_script, &workspace_root.path, - &glob_resolver, + glob_resolver, ) .map_err(|err| TaskGraphLoadError::ResolveConfigError { error: err, diff --git a/crates/vt_plan/src/context.rs b/crates/vt_plan/src/context.rs index 21da89138..3173a8787 100644 --- a/crates/vt_plan/src/context.rs +++ b/crates/vt_plan/src/context.rs @@ -53,8 +53,7 @@ pub struct PlanContext<'a> { indexed_task_graph: &'a IndexedTaskGraph, - /// Shared with every nested context, so each glob in synthetic commands' cache configs is - /// parsed once per plan. + /// The session's glob resolver, shared with task graph loading and every nested context. glob_resolver: Rc, /// Final resolved global cache config, combining the graph's config with any CLI override. @@ -81,12 +80,17 @@ pub struct PlanContext<'a> { } impl<'a> PlanContext<'a> { + #[expect( + clippy::too_many_arguments, + reason = "each argument initializes its own context field" + )] pub fn new( workspace_path: &'a Arc, cwd: Arc, envs: Arc>, Arc>>, callbacks: &'a mut (dyn PlanRequestParser + 'a), indexed_task_graph: &'a IndexedTaskGraph, + glob_resolver: Rc, resolved_global_cache: ResolvedGlobalCacheConfig, parent_query: Arc, ) -> Self { @@ -97,7 +101,7 @@ impl<'a> PlanContext<'a> { callbacks, task_call_stack: Vec::new(), indexed_task_graph, - glob_resolver: Rc::default(), + glob_resolver, extra_args: Arc::default(), resolved_global_cache, resolved_remote_cache: None, diff --git a/crates/vt_plan/src/lib.rs b/crates/vt_plan/src/lib.rs index 61d5f424f..56f8efd94 100644 --- a/crates/vt_plan/src/lib.rs +++ b/crates/vt_plan/src/lib.rs @@ -10,7 +10,7 @@ pub mod plan_request; mod ps1_shim; pub mod remote_cache; -use std::{collections::BTreeMap, ffi::OsStr, fmt::Debug, sync::Arc}; +use std::{collections::BTreeMap, ffi::OsStr, fmt::Debug, rc::Rc, sync::Arc}; use context::PlanContext; pub use envs::MARKER_ENV_NAME; @@ -206,6 +206,7 @@ pub async fn plan_query( envs: &Arc>, Arc>>, plan_request_parser: &mut (dyn PlanRequestParser + '_), task_graph_loader: &mut (dyn TaskGraphLoader + '_), + glob_resolver: &Rc, ) -> Result { let indexed_task_graph = task_graph_loader.load_task_graph().await?; @@ -219,6 +220,7 @@ pub async fn plan_query( Arc::clone(envs), plan_request_parser, indexed_task_graph, + Rc::clone(glob_resolver), resolved_global_cache, Arc::clone(&query), ); @@ -258,6 +260,7 @@ pub fn plan_synthetic( cwd: &Arc, synthetic_plan_request: SyntheticPlanRequest, cache_key: Arc<[Str]>, + glob_resolver: &GlobResolver, ) -> Result { let execution_cache_key = cache_metadata::ExecutionCacheKey::ExecAPI(cache_key); plan_synthetic_request( @@ -269,6 +272,6 @@ pub fn plan_synthetic( cwd, ParentCacheConfig::None, None, - &GlobResolver::default(), + glob_resolver, ) } From f13f1e8a47a32b5f0e0088c68651f0a4625917ed Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 26 Oct 2026 15:14:52 +0900 Subject: [PATCH 2/2] docs(changelog): add #890 to the cache glob parsing entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da5fb9cf..b4c205b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -- **Perf** `vp run` starts faster in workspaces whose tasks share cache `input` and `output` patterns: each distinct pattern is now parsed once instead of once for every task that uses it. In a 138-package workspace, this saves about 2 seconds of CPU on every run ([#887](https://github.com/voidzero-dev/vite-task/pull/887)). +- **Perf** `vp run` starts faster in workspaces whose tasks share cache `input` and `output` patterns: each distinct pattern is now parsed once instead of once for every task that uses it. In a 138-package workspace, this saves about 2 seconds of CPU on every run ([#887](https://github.com/voidzero-dev/vite-task/pull/887), [#890](https://github.com/voidzero-dev/vite-task/pull/890)). - **Perf** When tasks miss the cache, `vp run` no longer waits for the disk after saving each task's result, so the run finishes sooner. If the computer loses power or the operating system crashes during `vp run`, the cache entries that the run saved can be lost, and those tasks run again next time instead of hitting the cache ([#871](https://github.com/voidzero-dev/vite-task/pull/871)). - **Fixed** Running `vp run` in a package that sits inside a workspace folder but isn't listed in its `pnpm-workspace.yaml` `packages` or `package.json` `workspaces` now runs that package's own tasks, instead of treating the enclosing workspace as its root. When both configurations exist, `pnpm-workspace.yaml` takes precedence ([#859](https://github.com/voidzero-dev/vite-task/pull/859)). - **Fixed** A mistyped or misplaced setting in a task now gets an error that names it. For example, `cache: { outputs: [...] }` now fails with an `unknown field` error that names `outputs` and lists the accepted settings, instead of `data did not match any variant of untagged enum UserTaskDefinition` ([vite-plus#2635](https://github.com/voidzero-dev/vite-plus/issues/2635), [#815](https://github.com/voidzero-dev/vite-task/pull/815)).