From 89fb1359bf95433aaeffb71df4f7e94e75d13739 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 17 Aug 2026 14:23:23 +0800 Subject: [PATCH 1/6] feat(fspy): let the caller size the tracking channel The shared memory a tracked run reports its file accesses through was a constant in `fspy`, four gibibytes wide. How many accesses a program makes is the runner's business rather than the tracer's, and nothing could ask for a different size, so no test could put a task in front of a channel too small for it. `Command::shm_capacity` sets it, and the runner reads `VP_RUN_INTERNAL_FSPY_SHM_CAPACITY` for the value, keeping the same four gibibytes when the variable is unset. The variable is internal: it exists so a test can shrink the channel until a task overruns it, and nothing outside this repository should set it. A builder method rather than a second argument to `Command::new`, because the benchmark measures both revisions of `fspy` with a single launcher, overlaying the head's launcher source onto the baseline checkout. A launcher calling a signature only the head has cannot build the baseline arm. Leaving `new` alone also keeps the e2e tool, the examples and fspy's own tests out of this, since none of them care what size they get. The e2e case that comes with it stats one 2 MiB path, the largest single record tracking can be asked to hold, under a 64 MiB channel. That leaves room to spare, so the run caches like any other, which is what tells us the size arrived. The interesting case, a channel with no room for the record, has to wait: today it aborts the task process, and the panic it prints carries a thread id, a toolchain path, a backtrace and a platform's own abort code, none of which snapshot the same way twice. `vtt stat_long_filename` needed one fix to run there at all. Windows reports an over-long name as `ERROR_FILENAME_EXCED_RANGE`, which arrives as `InvalidFilename` rather than the `ENAMETOOLONG` unix returns, so the command exited 1 where it means to carry on: it exists to have the access attempted and recorded, not to find a file. Co-Authored-By: Claude Opus 5 --- crates/fspy/src/command.rs | 26 ++++++++++ crates/fspy/src/ipc.rs | 5 -- crates/fspy/src/lib.rs | 2 +- crates/fspy/src/unix/mod.rs | 4 +- crates/fspy/src/windows/mod.rs | 9 ++-- crates/vt/src/session/execute/spawn.rs | 19 +++++++ crates/vt_bin/src/vtt/stat_long_filename.rs | 19 ++++--- .../fixtures/fspy_shm_capacity/package.json | 4 ++ .../fixtures/fspy_shm_capacity/snapshots.toml | 26 ++++++++++ ...capacity_env_sizes_the_tracking_channel.md | 49 +++++++++++++++++++ .../fixtures/fspy_shm_capacity/vite-task.json | 8 +++ 11 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/package.json create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md create mode 100644 crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index fb150b26c..4684a01bb 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -12,9 +12,23 @@ use tokio_util::sync::CancellationToken; use crate::{SPY_IMPL, TrackedChild, error::SpawnError}; +/// Shared memory for a tracked run's file-access records when the caller +/// does not say otherwise. +/// +/// 4 GiB of sparse address space: none of it becomes real memory until +/// records land in it, and it leaves room for tens of millions of +/// accesses. +pub const DEFAULT_SHM_CAPACITY: usize = 4 << 30; + #[derive(derive_more::Debug)] pub struct Command { program: OsString, + /// Bytes of shared memory for this run's file-access records. + #[cfg_attr( + target_env = "musl", + expect(dead_code, reason = "musl builds track through seccomp, with no channel to size") + )] + pub(crate) shm_capacity: usize, args: Vec, envs: FxHashMap, cwd: Option, @@ -37,6 +51,7 @@ impl Command { pub fn new>(program: P) -> Self { Self { program: program.as_ref().to_os_string(), + shm_capacity: DEFAULT_SHM_CAPACITY, args: Vec::new(), envs: FxHashMap::default(), cwd: None, @@ -113,6 +128,17 @@ impl Command { self } + /// Sizes the shared memory this run's file-access records go through, + /// in bytes. + /// + /// How many accesses a program makes is the caller's business rather + /// than this crate's, so a caller that knows better than + /// [`DEFAULT_SHM_CAPACITY`] says so here. + pub const fn shm_capacity(&mut self, bytes: usize) -> &mut Self { + self.shm_capacity = bytes; + self + } + pub fn env(&mut self, key: K, val: V) -> &mut Self where K: AsRef, diff --git a/crates/fspy/src/ipc.rs b/crates/fspy/src/ipc.rs index 51d498600..af10a29b3 100644 --- a/crates/fspy/src/ipc.rs +++ b/crates/fspy/src/ipc.rs @@ -6,11 +6,6 @@ use fspy_shared::ipc::{ }; use tokio::task::spawn_blocking; -// Shared memory size for storing path accesses. -// 4 GiB is large enough to store path accesses in almost any realistic scenario. -// This doesn't allocate physical memory until it's actually used. -pub const SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024; - #[ouroboros::self_referencing] pub struct OwnedReceiverLockGuard { /// Owns the shared memory diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index 6c89414ba..d8b58b46c 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -19,7 +19,7 @@ mod command; use std::{env::temp_dir, fs::create_dir, io, process::ExitStatus, sync::LazyLock}; -pub use command::Command; +pub use command::{Command, DEFAULT_SHM_CAPACITY}; pub use fspy_shared::ipc::{AccessMode, PathAccess}; use futures_util::future::BoxFuture; pub use os_impl::PathAccessIterable; diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index f1d657436..2c97aad42 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -25,7 +25,7 @@ use tokio::task::spawn_blocking; use tokio_util::sync::CancellationToken; #[cfg(not(target_env = "musl"))] -use crate::ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}; +use crate::ipc::OwnedReceiverLockGuard; use crate::{ChildTermination, Command, TrackedChild, arena::PathAccessArena, error::SpawnError}; #[derive(Debug)] @@ -80,7 +80,7 @@ impl SpyImpl { #[cfg(not(target_env = "musl"))] let (ipc_channel_conf, ipc_receiver) = - channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreation)?; + channel(command.shm_capacity).map_err(SpawnError::ChannelCreation)?; let payload = Payload { #[cfg(not(target_env = "musl"))] diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index c468888a6..66966e67f 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -21,10 +21,8 @@ use winapi::{ use winsafe::co::{CP, WC}; use crate::{ - ChildTermination, TrackedChild, - command::Command, - error::SpawnError, - ipc::{OwnedReceiverLockGuard, SHM_CAPACITY}, + ChildTermination, TrackedChild, command::Command, error::SpawnError, + ipc::OwnedReceiverLockGuard, }; const INTERPOSE_CDYLIB: Artifact = @@ -82,12 +80,13 @@ impl SpyImpl { ) -> Result { let ansi_dll_path_with_nul = Arc::clone(&self.ansi_dll_path_with_nul); command.env("FSPY", "1"); + let shm_capacity = command.shm_capacity; let mut command = command.into_tokio_command(); command.creation_flags(CREATE_SUSPENDED); let (channel_conf, receiver) = - channel(SHM_CAPACITY).map_err(SpawnError::ChannelCreation)?; + channel(shm_capacity).map_err(SpawnError::ChannelCreation)?; let mut spawn_success = false; let spawn_success = &mut spawn_success; diff --git a/crates/vt/src/session/execute/spawn.rs b/crates/vt/src/session/execute/spawn.rs index adff8aac9..fe1dbd58a 100644 --- a/crates/vt/src/session/execute/spawn.rs +++ b/crates/vt/src/session/execute/spawn.rs @@ -14,6 +14,24 @@ use tokio::process::{ChildStderr, ChildStdout}; use tokio_util::sync::CancellationToken; use vt_plan::SpawnCommand; +/// Sets the shared memory a tracked task reports its file accesses +/// through, in bytes, in place of the size fspy would pick. Internal: it +/// exists so tests can shrink the channel until a task overruns it, and +/// nothing outside this repository should set it. +#[cfg(fspy)] +const FSPY_SHM_CAPACITY_ENV: &str = "VP_RUN_INTERNAL_FSPY_SHM_CAPACITY"; + +/// The shared memory each tracked task gets. Read once, since a run's +/// tasks all get the same size. +#[cfg(fspy)] +static FSPY_SHM_CAPACITY: std::sync::LazyLock = std::sync::LazyLock::new(|| { + std::env::var_os(FSPY_SHM_CAPACITY_ENV).map_or(fspy::DEFAULT_SHM_CAPACITY, |value| { + value.to_str().and_then(|value| value.parse().ok()).unwrap_or_else(|| { + panic!("{FSPY_SHM_CAPACITY_ENV} is not a byte count: {}", value.display()) + }) + }) +}); + /// How the child's stdin/stdout/stderr are configured. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SpawnStdio { @@ -100,6 +118,7 @@ where V: AsRef, { let mut fspy_cmd = fspy::Command::new(cmd.program_path.as_path()); + fspy_cmd.shm_capacity(*FSPY_SHM_CAPACITY); fspy_cmd.args(cmd.args.iter().map(vt_str::Str::as_str)); fspy_cmd.envs(cmd.spawn_envs.iter()); fspy_cmd.envs(extra_envs); diff --git a/crates/vt_bin/src/vtt/stat_long_filename.rs b/crates/vt_bin/src/vtt/stat_long_filename.rs index 1b44b1453..1a370ef7e 100644 --- a/crates/vt_bin/src/vtt/stat_long_filename.rs +++ b/crates/vt_bin/src/vtt/stat_long_filename.rs @@ -24,16 +24,23 @@ fn access_generated_path( let path = generated_path(count); match metadata(&path) { Ok(()) => Ok(()), - Err(error) - if error.kind() == io::ErrorKind::NotFound - || error.raw_os_error() == Some(libc::ENAMETOOLONG) => - { - Ok(()) - } + Err(error) if is_absent_or_too_long(&error) => Ok(()), Err(error) => Err(error), } } +/// Whether the platform said the file is not there, or that the name is +/// longer than it accepts. Either is the expected answer: this command +/// exists to have the access attempted and recorded, not to find a file. +/// +/// Windows reports an over-long name as `ERROR_FILENAME_EXCED_RANGE`, +/// which reaches here as [`io::ErrorKind::InvalidFilename`] rather than as +/// the `ENAMETOOLONG` unix returns. +fn is_absent_or_too_long(error: &io::Error) -> bool { + matches!(error.kind(), io::ErrorKind::NotFound | io::ErrorKind::InvalidFilename) + || error.raw_os_error() == Some(libc::ENAMETOOLONG) +} + fn metadata(path: &str) -> io::Result<()> { std::fs::metadata(path).map(|_| ()) } diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/package.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/package.json new file mode 100644 index 000000000..5fe27cb1c --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/package.json @@ -0,0 +1,4 @@ +{ + "name": "fspy-shm-capacity", + "private": true +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml new file mode 100644 index 000000000..7402b876e --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml @@ -0,0 +1,26 @@ +[[e2e]] +name = "shm_capacity_env_sizes_the_tracking_channel" +comment = """ +`VP_RUN_INTERNAL_FSPY_SHM_CAPACITY` sizes the shared memory a tracked task reports its file accesses through. The task stats one 2 MiB path, which is the largest single record tracking can be asked to hold, and 64 MiB holds it comfortably, so the run caches like any other. + +Setting the capacity below that record is what the knob exists for. That case has to wait: a channel with no room for a record currently aborts the task process, and the panic it prints carries a thread id, a toolchain path, a backtrace and a platform's own abort code, none of which snapshot the same way twice. +""" +steps = [ + { argv = [ + "vt", + "run", + "-v", + "stat", + ], envs = [ + [ + "VP_RUN_INTERNAL_FSPY_SHM_CAPACITY", + "67108864", + ], + ], comment = "64 MiB, room to spare for a 2 MiB record" }, + { argv = [ + "vt", + "run", + "-v", + "stat", + ], comment = "replayed from the entry the first run stored" }, +] diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md new file mode 100644 index 000000000..c44129291 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md @@ -0,0 +1,49 @@ +# shm_capacity_env_sizes_the_tracking_channel + +`VP_RUN_INTERNAL_FSPY_SHM_CAPACITY` sizes the shared memory a tracked task reports its file accesses through. The task stats one 2 MiB path, which is the largest single record tracking can be asked to hold, and 64 MiB holds it comfortably, so the run caches like any other. + +Setting the capacity below that record is what the knob exists for. That case has to wait: a channel with no room for a record currently aborts the task process, and the panic it prints carries a thread id, a toolchain path, a backtrace and a platform's own abort code, none of which snapshot the same way twice. + +## `VP_RUN_INTERNAL_FSPY_SHM_CAPACITY=67108864 vt run -v stat` + +64 MiB, room to spare for a 2 MiB record + +``` +$ vtt stat_long_filename 2097152 + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] fspy-shm-capacity#stat: $ vtt stat_long_filename 2097152 ✓ + → Cache miss: no previous cache entry found +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## `vt run -v stat` + +replayed from the entry the first run stored + +``` +$ vtt stat_long_filename 2097152 ◉ cache hit, replaying + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 1 cache hits • 0 cache misses +Performance: 100% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] fspy-shm-capacity#stat: $ vtt stat_long_filename 2097152 ✓ + → Cache hit - output replayed - +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json new file mode 100644 index 000000000..c9075fd16 --- /dev/null +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "stat": { + "command": "vtt stat_long_filename 2097152", + "cache": true + } + } +} From 8f57ac50b7c6192ed08c798acc490663ab2eab28 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 17 Aug 2026 14:27:32 +0800 Subject: [PATCH 2/6] fix(fspy): quiet two checks the first push tripped Win32 spells `ERROR_FILENAME_EXCED_RANGE` without the second E, and the comment naming it is more use to a reader than the spelling checker is, so the word joins the allowed list beside the other Windows one. The `shm_capacity` field needed no musl exemption after all. It is read there, by the setter, so claiming it is dead made the expectation unfulfilled instead. Co-Authored-By: Claude Opus 5 --- .typos.toml | 2 ++ crates/fspy/src/command.rs | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.typos.toml b/.typos.toml index 141848dc3..e7642038d 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,6 +1,8 @@ [default.extend-words] ratatui = "ratatui" PUNICODE = "PUNICODE" +# Win32 spells `ERROR_FILENAME_EXCED_RANGE` this way. +EXCED = "EXCED" [files] extend-exclude = [ diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 4684a01bb..1ff2c4a9a 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -24,10 +24,6 @@ pub const DEFAULT_SHM_CAPACITY: usize = 4 << 30; pub struct Command { program: OsString, /// Bytes of shared memory for this run's file-access records. - #[cfg_attr( - target_env = "musl", - expect(dead_code, reason = "musl builds track through seccomp, with no channel to size") - )] pub(crate) shm_capacity: usize, args: Vec, envs: FxHashMap, From 97b7c3b175a631bed56e56da460a5d26b63ff4ac Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 17 Aug 2026 14:37:37 +0800 Subject: [PATCH 3/6] test(e2e): drive the channel by record count, not record size Windows never overran the small channel the first version of this case set up, because a path record cannot get large enough there. A path reaches the tracer through a `UNICODE_STRING`, whose length field is a `u16`, so however long a name the caller asks for, no single record exceeds 64 KiB. Its 1 MiB channel had room to spare, tracking came back complete, and the run cached. Record count is the portable lever, and the slot table makes it exact: one slot per 64 bytes of the region, so a channel of a given size admits a known number of records whatever their paths look like. `vtt stat-many` makes as many accesses as asked for, under distinct names so none can fold into one record, and prints last to show the process outlived them. The case skips musl, which has no preload: those builds collect through the seccomp supervisor, on the runner's own side of the boundary, so there is no shared-memory channel there to fill. Co-Authored-By: Claude Opus 5 --- crates/vt_bin/src/vtt/main.rs | 4 ++- crates/vt_bin/src/vtt/stat_many.rs | 25 +++++++++++++++++++ .../fixtures/fspy_shm_capacity/snapshots.toml | 9 ++++--- ...capacity_env_sizes_the_tracking_channel.md | 18 +++++++------ .../fixtures/fspy_shm_capacity/vite-task.json | 2 +- 5 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 crates/vt_bin/src/vtt/stat_many.rs diff --git a/crates/vt_bin/src/vtt/main.rs b/crates/vt_bin/src/vtt/main.rs index 65a12f8b9..262840135 100644 --- a/crates/vt_bin/src/vtt/main.rs +++ b/crates/vt_bin/src/vtt/main.rs @@ -27,6 +27,7 @@ mod rm; mod small_dev_shm; mod stat_file; mod stat_long_filename; +mod stat_many; mod touch_file; mod write_file; @@ -35,7 +36,7 @@ fn main() { if args.len() < 2 { eprintln!("Usage: vtt [args...]"); eprintln!( - "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, small_dev_shm, stat-file, stat_long_filename, touch-file, write-file" + "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, small_dev_shm, stat-file, stat_long_filename, stat-many, touch-file, write-file" ); std::process::exit(1); } @@ -76,6 +77,7 @@ fn main() { Ok(()) } "stat_long_filename" => stat_long_filename::run(&args[2..]), + "stat-many" => stat_many::run(&args[2..]), "touch-file" => touch_file::run(&args[2..]), "write-file" => write_file::run(&args[2..]), other => { diff --git a/crates/vt_bin/src/vtt/stat_many.rs b/crates/vt_bin/src/vtt/stat_many.rs new file mode 100644 index 000000000..1ecf31b01 --- /dev/null +++ b/crates/vt_bin/src/vtt/stat_many.rs @@ -0,0 +1,25 @@ +//! Stats a run of generated names, to make a known number of tracked file +//! accesses. The names are missing on purpose, and each one differs from +//! the last: an access is recorded whether or not the file is there, and +//! distinct names cannot be folded into one record. +//! +//! A count is the portable way to give tracking more than it can hold. +//! Record size is not: on Windows a path arrives through a +//! `UNICODE_STRING`, whose length is a `u16`, so no single record there can +//! exceed 64 KiB however long a name the caller asks for. + +use std::error::Error; + +const USAGE: &str = "Usage: vtt stat-many "; + +pub fn run(args: &[String]) -> Result<(), Box> { + let [count] = args else { return Err(USAGE.into()) }; + let count: usize = count.parse().map_err(|_| USAGE)?; + for index in 0..count { + let _ = std::fs::metadata(format!("vtt-stat-many-{index}")); + } + // Printing last proves the process survived every one of them, which is + // what a channel that has stopped accepting records must not disturb. + println!("stat {count}"); + Ok(()) +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml index 7402b876e..6ce6bb943 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots.toml @@ -1,10 +1,13 @@ [[e2e]] name = "shm_capacity_env_sizes_the_tracking_channel" comment = """ -`VP_RUN_INTERNAL_FSPY_SHM_CAPACITY` sizes the shared memory a tracked task reports its file accesses through. The task stats one 2 MiB path, which is the largest single record tracking can be asked to hold, and 64 MiB holds it comfortably, so the run caches like any other. +`VP_RUN_INTERNAL_FSPY_SHM_CAPACITY` sizes the shared memory a tracked task reports its file accesses through. The task makes twenty thousand of them, and 64 MiB holds every one, so the run caches like any other. -Setting the capacity below that record is what the knob exists for. That case has to wait: a channel with no room for a record currently aborts the task process, and the panic it prints carries a thread id, a toolchain path, a backtrace and a platform's own abort code, none of which snapshot the same way twice. +Setting the capacity below what the task needs is what the knob exists for. That case has to wait: a channel with no room for a record currently aborts the task process, and the panic it prints carries a thread id, a toolchain path, a backtrace and a platform's own abort code, none of which snapshot the same way twice. + +Not on musl, which has no preload: those builds collect through the seccomp supervisor, on the runner's own side of the boundary, so they have no shared-memory channel to fill. """ +cfg = 'not(target_env = "musl")' steps = [ { argv = [ "vt", @@ -16,7 +19,7 @@ steps = [ "VP_RUN_INTERNAL_FSPY_SHM_CAPACITY", "67108864", ], - ], comment = "64 MiB, room to spare for a 2 MiB record" }, + ], comment = "64 MiB, room for every access" }, { argv = [ "vt", "run", diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md index c44129291..f28d778bc 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/snapshots/shm_capacity_env_sizes_the_tracking_channel.md @@ -1,15 +1,18 @@ # shm_capacity_env_sizes_the_tracking_channel -`VP_RUN_INTERNAL_FSPY_SHM_CAPACITY` sizes the shared memory a tracked task reports its file accesses through. The task stats one 2 MiB path, which is the largest single record tracking can be asked to hold, and 64 MiB holds it comfortably, so the run caches like any other. +`VP_RUN_INTERNAL_FSPY_SHM_CAPACITY` sizes the shared memory a tracked task reports its file accesses through. The task makes twenty thousand of them, and 64 MiB holds every one, so the run caches like any other. -Setting the capacity below that record is what the knob exists for. That case has to wait: a channel with no room for a record currently aborts the task process, and the panic it prints carries a thread id, a toolchain path, a backtrace and a platform's own abort code, none of which snapshot the same way twice. +Setting the capacity below what the task needs is what the knob exists for. That case has to wait: a channel with no room for a record currently aborts the task process, and the panic it prints carries a thread id, a toolchain path, a backtrace and a platform's own abort code, none of which snapshot the same way twice. + +Not on musl, which has no preload: those builds collect through the seccomp supervisor, on the runner's own side of the boundary, so they have no shared-memory channel to fill. ## `VP_RUN_INTERNAL_FSPY_SHM_CAPACITY=67108864 vt run -v stat` -64 MiB, room to spare for a 2 MiB record +64 MiB, room for every access ``` -$ vtt stat_long_filename 2097152 +$ vtt stat-many 20000 +stat 20000 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -21,7 +24,7 @@ Performance: 0% cache hit rate Task Details: ──────────────────────────────────────────────── - [1] fspy-shm-capacity#stat: $ vtt stat_long_filename 2097152 ✓ + [1] fspy-shm-capacity#stat: $ vtt stat-many 20000 ✓ → Cache miss: no previous cache entry found ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` @@ -31,7 +34,8 @@ Task Details: replayed from the entry the first run stored ``` -$ vtt stat_long_filename 2097152 ◉ cache hit, replaying +$ vtt stat-many 20000 ◉ cache hit, replaying +stat 20000 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -43,7 +47,7 @@ Performance: 100% cache hit rate Task Details: ──────────────────────────────────────────────── - [1] fspy-shm-capacity#stat: $ vtt stat_long_filename 2097152 ✓ + [1] fspy-shm-capacity#stat: $ vtt stat-many 20000 ✓ → Cache hit - output replayed - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json index c9075fd16..fce05d408 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/fspy_shm_capacity/vite-task.json @@ -1,7 +1,7 @@ { "tasks": { "stat": { - "command": "vtt stat_long_filename 2097152", + "command": "vtt stat-many 20000", "cache": true } } From f8b1c2597da403f903e55766a5836d0ebf6176a4 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 17 Aug 2026 14:57:31 +0800 Subject: [PATCH 4/6] style(fspy): spell the default capacity as a byte count `4 << 30` says how the number is built; `4 * 1024 * 1024 * 1024` says what it is. Co-Authored-By: Claude Opus 5 --- crates/fspy/src/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 1ff2c4a9a..7481bd12a 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -18,7 +18,7 @@ use crate::{SPY_IMPL, TrackedChild, error::SpawnError}; /// 4 GiB of sparse address space: none of it becomes real memory until /// records land in it, and it leaves room for tens of millions of /// accesses. -pub const DEFAULT_SHM_CAPACITY: usize = 4 << 30; +pub const DEFAULT_SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024; #[derive(derive_more::Debug)] pub struct Command { From 3ee7b6caf1477f4d578076f9dd9a7bcc2445fda1 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 17 Aug 2026 15:05:09 +0800 Subject: [PATCH 5/6] refactor(fspy): read the channel size where the channel is made The size arrived through a builder on `fspy::Command`, a public default constant, and a `LazyLock` in the runner that read the override and passed it down. Three places to look, for a number with exactly one consumer. It now reads the override next to the `channel` call that uses it, and falls back to the default there. `Command` goes back to what it was, and so do the e2e tool, the examples, the benchmark launcher and fspy's own tests, none of which ever wanted a say in the size. The runner no longer names the variable at all, which also settles the musl question: `fspy::ipc` is already `cfg(not(target_env = "musl"))`, so the size lives behind the same gate as the channel it sizes. Co-Authored-By: Claude Opus 5 --- crates/fspy/src/command.rs | 22 --------------------- crates/fspy/src/ipc.rs | 27 ++++++++++++++++++++++++++ crates/fspy/src/lib.rs | 2 +- crates/fspy/src/unix/mod.rs | 2 +- crates/fspy/src/windows/mod.rs | 3 +-- crates/vt/src/session/execute/spawn.rs | 19 ------------------ 6 files changed, 30 insertions(+), 45 deletions(-) diff --git a/crates/fspy/src/command.rs b/crates/fspy/src/command.rs index 7481bd12a..fb150b26c 100644 --- a/crates/fspy/src/command.rs +++ b/crates/fspy/src/command.rs @@ -12,19 +12,9 @@ use tokio_util::sync::CancellationToken; use crate::{SPY_IMPL, TrackedChild, error::SpawnError}; -/// Shared memory for a tracked run's file-access records when the caller -/// does not say otherwise. -/// -/// 4 GiB of sparse address space: none of it becomes real memory until -/// records land in it, and it leaves room for tens of millions of -/// accesses. -pub const DEFAULT_SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024; - #[derive(derive_more::Debug)] pub struct Command { program: OsString, - /// Bytes of shared memory for this run's file-access records. - pub(crate) shm_capacity: usize, args: Vec, envs: FxHashMap, cwd: Option, @@ -47,7 +37,6 @@ impl Command { pub fn new>(program: P) -> Self { Self { program: program.as_ref().to_os_string(), - shm_capacity: DEFAULT_SHM_CAPACITY, args: Vec::new(), envs: FxHashMap::default(), cwd: None, @@ -124,17 +113,6 @@ impl Command { self } - /// Sizes the shared memory this run's file-access records go through, - /// in bytes. - /// - /// How many accesses a program makes is the caller's business rather - /// than this crate's, so a caller that knows better than - /// [`DEFAULT_SHM_CAPACITY`] says so here. - pub const fn shm_capacity(&mut self, bytes: usize) -> &mut Self { - self.shm_capacity = bytes; - self - } - pub fn env(&mut self, key: K, val: V) -> &mut Self where K: AsRef, diff --git a/crates/fspy/src/ipc.rs b/crates/fspy/src/ipc.rs index af10a29b3..a804fa8c3 100644 --- a/crates/fspy/src/ipc.rs +++ b/crates/fspy/src/ipc.rs @@ -6,6 +6,33 @@ use fspy_shared::ipc::{ }; use tokio::task::spawn_blocking; +/// Shared memory for one tracked run's file-access records. +/// +/// 4 GiB of sparse address space: none of it becomes real memory until +/// records land in it, and it leaves room for tens of millions of +/// accesses. +const DEFAULT_SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024; + +/// Overrides [`DEFAULT_SHM_CAPACITY`] with a byte count. Internal: it +/// exists so a test can shrink the region until a run overruns it, and +/// nothing outside this repository should set it. +const SHM_CAPACITY_ENV: &str = "VP_RUN_INTERNAL_FSPY_SHM_CAPACITY"; + +/// How much shared memory to give the next tracked run. +/// +/// # Panics +/// +/// When the override is set to something that is not a byte count. It is +/// ours to set, so a value we cannot read is a mistake worth stopping for +/// rather than quietly ignoring. +pub fn shm_capacity() -> usize { + std::env::var_os(SHM_CAPACITY_ENV).map_or(DEFAULT_SHM_CAPACITY, |value| { + value.to_str().and_then(|value| value.parse().ok()).unwrap_or_else(|| { + panic!("{SHM_CAPACITY_ENV} is not a byte count: {}", value.display()) + }) + }) +} + #[ouroboros::self_referencing] pub struct OwnedReceiverLockGuard { /// Owns the shared memory diff --git a/crates/fspy/src/lib.rs b/crates/fspy/src/lib.rs index d8b58b46c..6c89414ba 100644 --- a/crates/fspy/src/lib.rs +++ b/crates/fspy/src/lib.rs @@ -19,7 +19,7 @@ mod command; use std::{env::temp_dir, fs::create_dir, io, process::ExitStatus, sync::LazyLock}; -pub use command::{Command, DEFAULT_SHM_CAPACITY}; +pub use command::Command; pub use fspy_shared::ipc::{AccessMode, PathAccess}; use futures_util::future::BoxFuture; pub use os_impl::PathAccessIterable; diff --git a/crates/fspy/src/unix/mod.rs b/crates/fspy/src/unix/mod.rs index 2c97aad42..c612d00b7 100644 --- a/crates/fspy/src/unix/mod.rs +++ b/crates/fspy/src/unix/mod.rs @@ -80,7 +80,7 @@ impl SpyImpl { #[cfg(not(target_env = "musl"))] let (ipc_channel_conf, ipc_receiver) = - channel(command.shm_capacity).map_err(SpawnError::ChannelCreation)?; + channel(crate::ipc::shm_capacity()).map_err(SpawnError::ChannelCreation)?; let payload = Payload { #[cfg(not(target_env = "musl"))] diff --git a/crates/fspy/src/windows/mod.rs b/crates/fspy/src/windows/mod.rs index 66966e67f..2e6470d47 100644 --- a/crates/fspy/src/windows/mod.rs +++ b/crates/fspy/src/windows/mod.rs @@ -80,13 +80,12 @@ impl SpyImpl { ) -> Result { let ansi_dll_path_with_nul = Arc::clone(&self.ansi_dll_path_with_nul); command.env("FSPY", "1"); - let shm_capacity = command.shm_capacity; let mut command = command.into_tokio_command(); command.creation_flags(CREATE_SUSPENDED); let (channel_conf, receiver) = - channel(shm_capacity).map_err(SpawnError::ChannelCreation)?; + channel(crate::ipc::shm_capacity()).map_err(SpawnError::ChannelCreation)?; let mut spawn_success = false; let spawn_success = &mut spawn_success; diff --git a/crates/vt/src/session/execute/spawn.rs b/crates/vt/src/session/execute/spawn.rs index fe1dbd58a..adff8aac9 100644 --- a/crates/vt/src/session/execute/spawn.rs +++ b/crates/vt/src/session/execute/spawn.rs @@ -14,24 +14,6 @@ use tokio::process::{ChildStderr, ChildStdout}; use tokio_util::sync::CancellationToken; use vt_plan::SpawnCommand; -/// Sets the shared memory a tracked task reports its file accesses -/// through, in bytes, in place of the size fspy would pick. Internal: it -/// exists so tests can shrink the channel until a task overruns it, and -/// nothing outside this repository should set it. -#[cfg(fspy)] -const FSPY_SHM_CAPACITY_ENV: &str = "VP_RUN_INTERNAL_FSPY_SHM_CAPACITY"; - -/// The shared memory each tracked task gets. Read once, since a run's -/// tasks all get the same size. -#[cfg(fspy)] -static FSPY_SHM_CAPACITY: std::sync::LazyLock = std::sync::LazyLock::new(|| { - std::env::var_os(FSPY_SHM_CAPACITY_ENV).map_or(fspy::DEFAULT_SHM_CAPACITY, |value| { - value.to_str().and_then(|value| value.parse().ok()).unwrap_or_else(|| { - panic!("{FSPY_SHM_CAPACITY_ENV} is not a byte count: {}", value.display()) - }) - }) -}); - /// How the child's stdin/stdout/stderr are configured. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SpawnStdio { @@ -118,7 +100,6 @@ where V: AsRef, { let mut fspy_cmd = fspy::Command::new(cmd.program_path.as_path()); - fspy_cmd.shm_capacity(*FSPY_SHM_CAPACITY); fspy_cmd.args(cmd.args.iter().map(vt_str::Str::as_str)); fspy_cmd.envs(cmd.spawn_envs.iter()); fspy_cmd.envs(extra_envs); From 17a5afc9f5ec8ee8b4574e452786d85c150a5f5d Mon Sep 17 00:00:00 2001 From: wan9chi Date: Mon, 17 Aug 2026 15:05:10 +0800 Subject: [PATCH 6/6] refactor(vtt): fold stat_long_filename into stat-many Two commands stat generated names to be tracked; one varied the name's length and the other how many names. They are now `stat-many [name-length]`, which also puts the name in kebab case with every other subcommand. Count leads because it is the knob that travels. A long name only fills a channel on unix: on Windows a path reaches the tracer through a `UNICODE_STRING` whose length is a `u16`, so no single record there exceeds 64 KiB however long a name the caller asks for. Names now carry their index, so a run of them cannot collapse into one record, and padding fills out whatever length is asked for. The `/dev/shm` case keeps its one 1 MiB name as `stat-many 1 1048576`, and gains the trailing line that reports the process survived its accesses. Co-Authored-By: Claude Opus 5 --- crates/vt_bin/src/vtt/main.rs | 4 +- crates/vt_bin/src/vtt/stat_long_filename.rs | 46 ----------- crates/vt_bin/src/vtt/stat_many.rs | 81 +++++++++++++++---- .../snapshots/constrained_dev_shm.md | 3 +- .../constrained_dev_shm/vite-task.json | 2 +- 5 files changed, 70 insertions(+), 66 deletions(-) delete mode 100644 crates/vt_bin/src/vtt/stat_long_filename.rs diff --git a/crates/vt_bin/src/vtt/main.rs b/crates/vt_bin/src/vtt/main.rs index 262840135..dd36cadba 100644 --- a/crates/vt_bin/src/vtt/main.rs +++ b/crates/vt_bin/src/vtt/main.rs @@ -26,7 +26,6 @@ mod rm; #[cfg(target_os = "linux")] mod small_dev_shm; mod stat_file; -mod stat_long_filename; mod stat_many; mod touch_file; mod write_file; @@ -36,7 +35,7 @@ fn main() { if args.len() < 2 { eprintln!("Usage: vtt [args...]"); eprintln!( - "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, small_dev_shm, stat-file, stat_long_filename, stat-many, touch-file, write-file" + "Subcommands: barrier, check-tty, cp, exit, exit-on-ctrlc, grep-file, list-dir, mkdir, pipe-stdin, print, print-color, print-cwd, print-env, print-file, read-stdin, replace-file-content, rm, small_dev_shm, stat-file, stat-many, touch-file, write-file" ); std::process::exit(1); } @@ -76,7 +75,6 @@ fn main() { stat_file::run(&args[2..]); Ok(()) } - "stat_long_filename" => stat_long_filename::run(&args[2..]), "stat-many" => stat_many::run(&args[2..]), "touch-file" => touch_file::run(&args[2..]), "write-file" => write_file::run(&args[2..]), diff --git a/crates/vt_bin/src/vtt/stat_long_filename.rs b/crates/vt_bin/src/vtt/stat_long_filename.rs deleted file mode 100644 index 1a370ef7e..000000000 --- a/crates/vt_bin/src/vtt/stat_long_filename.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::{error::Error, io}; - -const USAGE: &str = "Usage: vtt stat_long_filename "; - -pub fn run(args: &[String]) -> Result<(), Box> { - let count = parse_count(args)?; - access_generated_path(count, metadata)?; - Ok(()) -} - -fn parse_count(args: &[String]) -> Result { - let [count] = args else { return Err(USAGE.to_owned()) }; - count.parse().map_err(|_| USAGE.to_owned()) -} - -fn generated_path(count: usize) -> String { - "x".repeat(count) -} - -fn access_generated_path( - count: usize, - mut metadata: impl FnMut(&str) -> io::Result<()>, -) -> io::Result<()> { - let path = generated_path(count); - match metadata(&path) { - Ok(()) => Ok(()), - Err(error) if is_absent_or_too_long(&error) => Ok(()), - Err(error) => Err(error), - } -} - -/// Whether the platform said the file is not there, or that the name is -/// longer than it accepts. Either is the expected answer: this command -/// exists to have the access attempted and recorded, not to find a file. -/// -/// Windows reports an over-long name as `ERROR_FILENAME_EXCED_RANGE`, -/// which reaches here as [`io::ErrorKind::InvalidFilename`] rather than as -/// the `ENAMETOOLONG` unix returns. -fn is_absent_or_too_long(error: &io::Error) -> bool { - matches!(error.kind(), io::ErrorKind::NotFound | io::ErrorKind::InvalidFilename) - || error.raw_os_error() == Some(libc::ENAMETOOLONG) -} - -fn metadata(path: &str) -> io::Result<()> { - std::fs::metadata(path).map(|_| ()) -} diff --git a/crates/vt_bin/src/vtt/stat_many.rs b/crates/vt_bin/src/vtt/stat_many.rs index 1ecf31b01..777d71d90 100644 --- a/crates/vt_bin/src/vtt/stat_many.rs +++ b/crates/vt_bin/src/vtt/stat_many.rs @@ -1,25 +1,76 @@ -//! Stats a run of generated names, to make a known number of tracked file -//! accesses. The names are missing on purpose, and each one differs from -//! the last: an access is recorded whether or not the file is there, and -//! distinct names cannot be folded into one record. +//! Stats generated names, to make a known number of tracked file accesses. //! -//! A count is the portable way to give tracking more than it can hold. -//! Record size is not: on Windows a path arrives through a -//! `UNICODE_STRING`, whose length is a `u16`, so no single record there can -//! exceed 64 KiB however long a name the caller asks for. +//! The names are missing on purpose: an access is recorded whether or not +//! the file is there, and this exists to have the access attempted, not to +//! find a file. Each name differs from the last, so no two can fold into +//! one record. +//! +//! Both knobs give tracking more than it can hold, and only one of them +//! travels. A count works everywhere. A long name does not: on Windows a +//! path reaches the tracer through a `UNICODE_STRING`, whose length is a +//! `u16`, so however long a name this asks for, no single record there +//! exceeds 64 KiB. -use std::error::Error; +use std::{error::Error, io}; -const USAGE: &str = "Usage: vtt stat-many "; +const USAGE: &str = "Usage: vtt stat-many [name-length]"; pub fn run(args: &[String]) -> Result<(), Box> { - let [count] = args else { return Err(USAGE.into()) }; - let count: usize = count.parse().map_err(|_| USAGE)?; + let (count, name_length) = parse_args(args)?; for index in 0..count { - let _ = std::fs::metadata(format!("vtt-stat-many-{index}")); + access_generated_path(index, name_length, metadata)?; } - // Printing last proves the process survived every one of them, which is - // what a channel that has stopped accepting records must not disturb. + // Printing last proves the process survived every one of them, which a + // channel that has stopped accepting records must not disturb. println!("stat {count}"); Ok(()) } + +fn parse_args(args: &[String]) -> Result<(usize, usize), String> { + let (count, name_length) = match args { + [count] => (count, None), + [count, name_length] => (count, Some(name_length)), + _ => return Err(USAGE.to_owned()), + }; + let count = count.parse().map_err(|_| USAGE.to_owned())?; + let name_length = + name_length.map(|length| length.parse()).transpose().map_err(|_| USAGE.to_owned())?; + Ok((count, name_length.unwrap_or(0))) +} + +/// A name unique to `index`, padded out to `name_length` when that leaves +/// room for padding. A length short enough to truncate the index would +/// hand two accesses the same name, so the index always survives. +fn generated_path(index: usize, name_length: usize) -> String { + let name = std::format!("vtt-stat-many-{index}"); + let padding = name_length.saturating_sub(name.len()); + name + &"x".repeat(padding) +} + +fn access_generated_path( + index: usize, + name_length: usize, + mut metadata: impl FnMut(&str) -> io::Result<()>, +) -> io::Result<()> { + let path = generated_path(index, name_length); + match metadata(&path) { + Ok(()) => Ok(()), + Err(error) if is_absent_or_too_long(&error) => Ok(()), + Err(error) => Err(error), + } +} + +/// Whether the platform said the file is not there, or that the name is +/// longer than it accepts. Either is the expected answer. +/// +/// Windows reports an over-long name as `ERROR_FILENAME_EXCED_RANGE`, +/// which reaches here as [`io::ErrorKind::InvalidFilename`] rather than as +/// the `ENAMETOOLONG` unix returns. +fn is_absent_or_too_long(error: &io::Error) -> bool { + matches!(error.kind(), io::ErrorKind::NotFound | io::ErrorKind::InvalidFilename) + || error.raw_os_error() == Some(libc::ENAMETOOLONG) +} + +fn metadata(path: &str) -> io::Result<()> { + std::fs::metadata(path).map(|_| ()) +} diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/snapshots/constrained_dev_shm.md b/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/snapshots/constrained_dev_shm.md index 4dd59fa3b..183835957 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/snapshots/constrained_dev_shm.md +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/snapshots/constrained_dev_shm.md @@ -5,5 +5,6 @@ With fspy's shared-memory backing moved to memfd, file-access tracking succeeds ## `vtt small_dev_shm vt run stress` ``` -$ vtt stat_long_filename 1048576 +$ vtt stat-many 1 1048576 +stat 1 ``` diff --git a/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/vite-task.json b/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/vite-task.json index 0415ae320..aa51997cc 100644 --- a/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/vite-task.json +++ b/crates/vt_bin/tests/e2e_snapshots/fixtures/constrained_dev_shm/vite-task.json @@ -1,7 +1,7 @@ { "tasks": { "stress": { - "command": "vtt stat_long_filename 1048576", + "command": "vtt stat-many 1 1048576", "cache": true } }