Skip to content

Commit fed3a82

Browse files
committed
fix: exclude cache directory from fspy tracking to prevent spurious cache misses
Tools like oxlint traverse node_modules/ during execution, which causes fspy to record reads of files in the cache directory (SQLite DB, last-summary.json). When these cache files change between runs, fspy reports them as changed inputs, causing spurious cache misses. Thread cache_dir_relative through Session -> ExecutionContext -> spawn_with_tracking to filter out cache directory accesses alongside the existing .git exclusion. Also adds summary-output e2e test fixture with 10 test cases covering compact summary, --verbose, and --last-details.
1 parent fcc662b commit fed3a82

19 files changed

Lines changed: 343 additions & 6 deletions

crates/vite_task/src/session/execute/mod.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{process::Stdio, sync::Arc};
55

66
use futures_util::FutureExt;
77
use tokio::io::AsyncWriteExt as _;
8-
use vite_path::AbsolutePath;
8+
use vite_path::{AbsolutePath, RelativePath};
99
use vite_task_plan::{
1010
ExecutionGraph, ExecutionItemDisplay, ExecutionItemKind, LeafExecutionKind, SpawnCommand,
1111
SpawnExecution,
@@ -56,6 +56,11 @@ struct ExecutionContext<'a> {
5656
/// Base path for resolving relative paths in cache entries.
5757
/// Typically the workspace root.
5858
cache_base_path: &'a Arc<AbsolutePath>,
59+
/// Cache directory path relative to the workspace root.
60+
/// Used to exclude cache directory accesses from fspy tracking (the cache
61+
/// directory is infrastructure, not a task input).
62+
/// `None` when the cache directory is outside the workspace (custom `VITE_CACHE_PATH`).
63+
cache_dir_relative: Option<&'a RelativePath>,
5964
}
6065

6166
impl ExecutionContext<'_> {
@@ -152,9 +157,14 @@ impl ExecutionContext<'_> {
152157
clippy::large_futures,
153158
reason = "spawn execution with cache management creates large futures"
154159
)]
155-
let _ =
156-
execute_spawn(leaf_reporter, spawn_execution, self.cache, self.cache_base_path)
157-
.await;
160+
let _ = execute_spawn(
161+
leaf_reporter,
162+
spawn_execution,
163+
self.cache,
164+
self.cache_base_path,
165+
self.cache_dir_relative,
166+
)
167+
.await;
158168
}
159169
}
160170
}
@@ -184,6 +194,7 @@ pub async fn execute_spawn(
184194
spawn_execution: &SpawnExecution,
185195
cache: &ExecutionCache,
186196
cache_base_path: &Arc<AbsolutePath>,
197+
cache_dir_relative: Option<&RelativePath>,
187198
) -> SpawnOutcome {
188199
let cache_metadata = spawn_execution.cache_metadata.as_ref();
189200

@@ -293,6 +304,7 @@ pub async fn execute_spawn(
293304
&mut stdio_config.stdout_writer,
294305
&mut stdio_config.stderr_writer,
295306
track_result_with_cache_metadata.as_mut().map(|(track_result, _)| track_result),
307+
cache_dir_relative,
296308
)
297309
.await
298310
{
@@ -418,6 +430,7 @@ impl Session<'_> {
418430
reporter: &mut *reporter,
419431
cache,
420432
cache_base_path: &self.workspace_path,
433+
cache_dir_relative: self.cache_dir_relative.as_deref(),
421434
};
422435

423436
// Execute the graph. Leaf-level errors are reported through the reporter

crates/vite_task/src/session/execute/spawn.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use fspy::AccessMode;
1111
use rustc_hash::FxHashSet;
1212
use serde::Serialize;
1313
use tokio::io::{AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
14-
use vite_path::{AbsolutePath, RelativePathBuf};
14+
use vite_path::{AbsolutePath, RelativePath, RelativePathBuf};
1515
use vite_task_plan::SpawnCommand;
1616

1717
use crate::collections::HashMap;
@@ -75,6 +75,7 @@ pub async fn spawn_with_tracking(
7575
stdout_writer: &mut (dyn AsyncWrite + Unpin),
7676
stderr_writer: &mut (dyn AsyncWrite + Unpin),
7777
track_result: Option<&mut SpawnTrackResult>,
78+
cache_dir_relative: Option<&RelativePath>,
7879
) -> anyhow::Result<SpawnResult> {
7980
/// The tracking state of the spawned process
8081
enum TrackingState<'a> {
@@ -206,6 +207,16 @@ pub async fn spawn_with_tracking(
206207
continue;
207208
}
208209

210+
// Skip cache directory accesses — the cache directory is infrastructure
211+
// (SQLite DB, last-summary.json), not a task input. Tools like oxlint may
212+
// traverse node_modules/ and read files in the cache directory, which would
213+
// otherwise cause spurious cache misses when those files change between runs.
214+
if let Some(cache_dir) = cache_dir_relative
215+
&& relative_path.as_path().strip_prefix(cache_dir).is_ok()
216+
{
217+
continue;
218+
}
219+
209220
if access.mode.contains(AccessMode::READ) {
210221
path_reads.entry(relative_path.clone()).or_insert(PathRead { read_dir_entries: false });
211222
}

crates/vite_task/src/session/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use reporter::{
1515
summary::{LastRunSummary, ReadSummaryError, format_full_summary},
1616
};
1717
use rustc_hash::FxHashMap;
18-
use vite_path::{AbsolutePath, AbsolutePathBuf};
18+
use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf};
1919
use vite_select::SelectItem;
2020
use vite_str::Str;
2121
use vite_task_graph::{
@@ -141,6 +141,11 @@ pub struct Session<'a> {
141141
/// processes (e.g., parallel `vp lib` commands) start simultaneously.
142142
cache: OnceCell<ExecutionCache>,
143143
cache_path: AbsolutePathBuf,
144+
/// Cache directory path relative to the workspace root.
145+
/// Used to exclude cache directory accesses from fspy tracking (the cache
146+
/// directory is infrastructure, not a task input).
147+
/// `None` when the cache directory is outside the workspace (custom `VITE_CACHE_PATH`).
148+
cache_dir_relative: Option<RelativePathBuf>,
144149
}
145150

146151
fn get_cache_path_of_workspace(workspace_root: &AbsolutePath) -> AbsolutePathBuf {
@@ -198,6 +203,12 @@ impl<'a> Session<'a> {
198203
let (workspace_root, _) = find_workspace_root(&cwd)?;
199204
let cache_path = get_cache_path_of_workspace(&workspace_root.path);
200205

206+
// Compute the cache directory path relative to the workspace root.
207+
// Used to exclude cache directory accesses from fspy tracking.
208+
// `None` if the cache path is outside the workspace (custom VITE_CACHE_PATH)
209+
// or if stripping the prefix fails.
210+
let cache_dir_relative = cache_path.strip_prefix(&workspace_root.path).ok().flatten();
211+
201212
// Prepend workspace's node_modules/.bin to PATH
202213
let workspace_node_modules_bin = workspace_root.path.join("node_modules").join(".bin");
203214
prepend_path_env(&mut envs, &workspace_node_modules_bin)?;
@@ -214,6 +225,7 @@ impl<'a> Session<'a> {
214225
plan_request_parser: PlanRequestParser { command_handler: callbacks.command_handler },
215226
cache: OnceCell::new(),
216227
cache_path,
228+
cache_dir_relative,
217229
})
218230
}
219231

@@ -545,6 +557,7 @@ impl<'a> Session<'a> {
545557
&spawn_execution,
546558
cache,
547559
&self.workspace_path,
560+
self.cache_dir_relative.as_deref(),
548561
)
549562
.await
550563
{
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "summary-output-test",
3+
"private": true
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "@summary/a",
3+
"scripts": {
4+
"build": "print built-a"
5+
}
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "@summary/b",
3+
"dependencies": {
4+
"@summary/a": "workspace:*"
5+
},
6+
"scripts": {
7+
"build": "print built-b"
8+
}
9+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- packages/*
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Tests for compact and verbose summary output
2+
3+
# Single task, cache miss → no summary at all
4+
[[e2e]]
5+
name = "single task cache miss shows no summary"
6+
cwd = "packages/a"
7+
steps = [
8+
"vp run build",
9+
]
10+
11+
# Single task, cache hit → compact one-liner
12+
[[e2e]]
13+
name = "single task cache hit shows compact summary"
14+
cwd = "packages/a"
15+
steps = [
16+
"vp run build # first run, cache miss",
17+
"vp run build # second run, cache hit → compact summary",
18+
]
19+
20+
# Multi-task (recursive), all cache miss → compact summary with 0 hits
21+
[[e2e]]
22+
name = "multi task all cache miss shows compact summary"
23+
steps = [
24+
"vp run -r build",
25+
]
26+
27+
# Multi-task (recursive), some cache hits → compact summary with hit count
28+
[[e2e]]
29+
name = "multi task with cache hits shows compact summary"
30+
steps = [
31+
"vp run -r build # first run, all miss",
32+
"vp run -r build # second run, all hit",
33+
]
34+
35+
# Single task with --verbose → full detailed summary
36+
[[e2e]]
37+
name = "single task verbose shows full summary"
38+
cwd = "packages/a"
39+
steps = [
40+
"vp run -v build",
41+
]
42+
43+
# Multi-task with --verbose → full detailed summary
44+
[[e2e]]
45+
name = "multi task verbose shows full summary"
46+
steps = [
47+
"vp run -r -v build",
48+
]
49+
50+
# Multi-task with --verbose after cache hits
51+
[[e2e]]
52+
name = "multi task verbose with cache hits shows full summary"
53+
steps = [
54+
"vp run -r build # first run, populate cache",
55+
"vp run -r -v build # second run, verbose with cache hits",
56+
]
57+
58+
# --last-details with no previous run
59+
[[e2e]]
60+
name = "last details with no previous run shows error"
61+
steps = [
62+
"vp run --last-details",
63+
]
64+
65+
# --last-details after a run shows saved summary
66+
[[e2e]]
67+
name = "last details after run shows saved summary"
68+
cwd = "packages/a"
69+
steps = [
70+
"vp run build # populate summary file",
71+
"vp run --last-details # display saved summary",
72+
]
73+
74+
# --last-details after a multi-task run
75+
[[e2e]]
76+
name = "last details after multi task run shows saved summary"
77+
steps = [
78+
"vp run -r build # populate summary file",
79+
"vp run --last-details # display saved summary",
80+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
---
5+
> vp run -r build # populate summary file
6+
~/packages/a$ print built-a
7+
built-a
8+
9+
~/packages/b$ print built-b
10+
built-b
11+
12+
---
13+
[vp run] 0/2 cache hit (0%). (Run `vp run --verbose` for full details)
14+
> vp run --last-details # display saved summary
15+
16+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17+
Vite+ Task RunnerExecution Summary
18+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19+
20+
Statistics: 2 tasks0 cache hits2 cache misses
21+
Performance: 0% cache hit rate
22+
23+
Task Details:
24+
────────────────────────────────────────────────
25+
[1] @summary/a#build: ~/packages/a$ print built-a
26+
Cache miss: no previous cache entry found
27+
·······················································
28+
[2] @summary/b#build: ~/packages/b$ print built-b
29+
Cache miss: no previous cache entry found
30+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
---
5+
> vp run build # populate summary file
6+
~/packages/a$ print built-a
7+
built-a
8+
> vp run --last-details # display saved summary
9+
10+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11+
Vite+ Task RunnerExecution Summary
12+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13+
14+
Statistics: 1 tasks0 cache hits1 cache misses
15+
Performance: 0% cache hit rate
16+
17+
Task Details:
18+
────────────────────────────────────────────────
19+
[1] @summary/a#build: ~/packages/a$ print built-a
20+
Cache miss: no previous cache entry found
21+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

0 commit comments

Comments
 (0)