Skip to content

Commit 41e4c55

Browse files
wan9chiGPT-5.6
andcommitted
perf(fspy): use sparse Windows shared memory
Co-authored-by: GPT-5.6 <gpt-5.6@openai.com>
1 parent 5d1ba2f commit 41e4c55

5 files changed

Lines changed: 151 additions & 101 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- **Fixed** Failures while waiting for a started task process to exit no longer incorrectly say the process failed to spawn ([#515](https://github.com/voidzero-dev/vite-task/pull/515)).
55
- **Fixed** Missing env vars requested through `@voidzero-dev/vite-task-client` now return `undefined` instead of `null`, preserving Vite production `NODE_ENV` semantics when builds run through `vp run` ([#508](https://github.com/voidzero-dev/vite-task/pull/508)).
66
- **Fixed** Linux file-access tracking no longer consumes the `/dev/shm` mount used by containers and Kubernetes runners ([#353](https://github.com/voidzero-dev/vite-task/issues/353)).
7+
- **Improved** Windows file-access tracking now uses sparse temporary backing files, avoiding an upfront 4 GiB disk allocation per task.
78
- **Fixed** Windows builds no longer hang on CI when a `node_modules/.bin` `.cmd` shim is routed through PowerShell: the npm/pnpm/yarn `.ps1` wrappers read stdin and block forever on a non-TTY pipe, so the PowerShell rewrite is now skipped when stdin is not an interactive terminal, falling back to the `.cmd` (which never reads stdin) ([#491](https://github.com/voidzero-dev/vite-task/pull/491)).
89
- **Added** First-party support for caching `vite build` with zero cache config, giving Vite projects correct cache hits out of the box ([vitejs/vite#22453](https://github.com/vitejs/vite/pull/22453)).
910
- **Added** Support for specifying tasks from dependency packages in `dependsOn`, such as `dependsOn: [{ "task": "build", "from": "dependencies" }]` ([#479](https://github.com/voidzero-dev/vite-task/pull/479)).

crates/fspy_shm/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ windows-sys = { workspace = true, features = [
2424
"Win32_Foundation",
2525
"Win32_Security",
2626
"Win32_Storage_FileSystem",
27+
"Win32_System_IO",
28+
"Win32_System_Ioctl",
2729
"Win32_System_Memory",
2830
] }
2931

crates/fspy_shm/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,9 @@ mod tests {
199199
write_byte(&owner, 0, 17);
200200
drop(owner);
201201

202-
// Windows keeps the named object alive while an opened view exists.
202+
// POSIX shm_unlink removes the name immediately. On Windows,
203+
// an existing mapped view keeps the named kernel object alive; fspy's
204+
// lock file, rather than this low-level mapping API, rejects late senders.
203205
#[cfg(not(target_os = "windows"))]
204206
assert!(open(&id, SIZE).is_err());
205207
assert_eq!(read_byte(&opened, 0), 17);

crates/fspy_shm/src/windows/mod.rs

Lines changed: 84 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ const BACKING_DIR: &str = "vite-task-fspy";
1919
pub struct Shm {
2020
id: String,
2121
view: MappedView,
22-
_mapping: OwnedHandle,
23-
backing_file: BackingFile,
22+
_mapping: Option<OwnedHandle>,
23+
// Owner mappings keep this file alive until their view and mapping handle drop.
24+
#[cfg_attr(
25+
not(test),
26+
expect(dead_code, reason = "the file is retained only for RAII cleanup")
27+
)]
28+
backing_file: Option<File>,
2429
}
2530

2631
/// A newly created shared-memory mapping.
@@ -29,7 +34,7 @@ pub struct CreatedShm {
2934
pub shm: Shm,
3035
}
3136

32-
/// Creates a file-backed named mapping of `size` bytes.
37+
/// Creates a sparse, temporary file-backed named mapping of `size` bytes.
3338
///
3439
/// # Errors
3540
///
@@ -52,14 +57,14 @@ fn create_with(size: usize, mut next_name: impl FnMut() -> String) -> io::Result
5257

5358
fn create_named(id: &str, size: u64) -> io::Result<Option<Shm>> {
5459
let path = backing_path(id)?;
55-
let backing_file = match BackingFile::create(path) {
60+
let backing_file = match create_backing_file(&path) {
5661
Ok(file) => file,
5762
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => return Ok(None),
5863
Err(error) => return Err(error),
5964
};
60-
backing_file.file.set_len(size)?;
65+
initialize_backing_file(&backing_file, size)?;
6166

62-
let mapping = match sys::create_file_mapping(&backing_file.file, id)? {
67+
let mapping = match sys::create_file_mapping(&backing_file, id)? {
6368
CreatedMapping::Created(mapping) => mapping,
6469
CreatedMapping::AlreadyExists => return Ok(None),
6570
};
@@ -68,29 +73,28 @@ fn create_named(id: &str, size: u64) -> io::Result<Option<Shm>> {
6873
})?;
6974
let view = MappedView::new(&mapping, len)?;
7075

71-
Ok(Some(Shm { id: id.to_owned(), view, _mapping: mapping, backing_file }))
76+
Ok(Some(Shm {
77+
id: id.to_owned(),
78+
view,
79+
_mapping: Some(mapping),
80+
backing_file: Some(backing_file),
81+
}))
7282
}
7383

7484
/// Opens the named mapping identified by `id`.
7585
///
7686
/// # Errors
7787
///
78-
/// Returns an error if the identifier is invalid, the owner has torn down the
79-
/// mapping, or its backing file has a different size.
88+
/// Returns an error if the identifier is invalid, the mapping is unavailable,
89+
/// or `size` cannot be mapped from it.
8090
pub fn open(id: &str, size: usize) -> io::Result<Shm> {
81-
let expected_size = valid_size(size)?;
82-
let path = backing_path(id)?;
83-
let backing_file = BackingFile::open(path)?;
84-
if backing_file.file.metadata()?.len() != expected_size {
85-
return Err(io::Error::new(
86-
io::ErrorKind::InvalidData,
87-
"shared-memory backing file has an unexpected size",
88-
));
89-
}
91+
valid_size(size)?;
92+
validate_id(id)?;
9093

9194
let mapping = sys::open_file_mapping(id)?;
9295
let view = MappedView::new(&mapping, size)?;
93-
Ok(Shm { id: id.to_owned(), view, _mapping: mapping, backing_file })
96+
drop(mapping);
97+
Ok(Shm { id: id.to_owned(), view, _mapping: None, backing_file: None })
9498
}
9599

96100
fn valid_size(size: usize) -> io::Result<u64> {
@@ -109,83 +113,37 @@ fn backing_dir() -> PathBuf {
109113
}
110114

111115
fn backing_path(id: &str) -> io::Result<PathBuf> {
116+
let suffix = validate_id(id)?;
117+
Ok(backing_dir().join(format!("{suffix}.shm")))
118+
}
119+
120+
fn validate_id(id: &str) -> io::Result<&str> {
112121
let suffix = id.strip_prefix(NAME_PREFIX).ok_or_else(invalid_id)?;
113122
let uuid = Uuid::parse_str(suffix).map_err(|_| invalid_id())?;
114123
if uuid.hyphenated().to_string() != suffix {
115124
return Err(invalid_id());
116125
}
117-
Ok(backing_dir().join(format!("{suffix}.shm")))
126+
Ok(suffix)
118127
}
119128

120129
fn invalid_id() -> io::Error {
121130
io::Error::new(io::ErrorKind::InvalidInput, "invalid Windows shared-memory identifier")
122131
}
123132

124-
struct BackingFile {
125-
file: File,
126-
owner_path: Option<PathBuf>,
127-
}
128-
129-
impl BackingFile {
130-
fn create(path: PathBuf) -> io::Result<Self> {
131-
let file = OpenOptions::new()
132-
.read(true)
133-
.write(true)
134-
.create_new(true)
135-
.share_mode(sys::SHARE_ALL)
136-
.attributes(sys::TEMPORARY)
137-
.open(&path)?;
138-
Ok(Self { file, owner_path: Some(path) })
139-
}
140-
141-
fn open(path: PathBuf) -> io::Result<Self> {
142-
let file = OpenOptions::new()
143-
.read(true)
144-
.write(true)
145-
.share_mode(sys::SHARE_ALL)
146-
.attributes(sys::TEMPORARY)
147-
.open(path)?;
148-
Ok(Self { file, owner_path: None })
149-
}
150-
151-
fn unlink(&mut self) {
152-
let Some(path) = self.owner_path.take() else {
153-
return;
154-
};
155-
156-
let deletion_file = OpenOptions::new()
157-
.access_mode(sys::DELETE_ACCESS)
158-
.share_mode(sys::SHARE_ALL)
159-
.attributes(sys::DELETE_ON_CLOSE)
160-
.open(&path);
161-
if let Ok(deletion_file) = deletion_file {
162-
let deleted_path = deleted_path(&path);
163-
if fs::rename(&path, deleted_path).is_err() {
164-
let _ = fs::remove_file(&path);
165-
}
166-
drop(deletion_file);
167-
} else {
168-
let _ = fs::remove_file(path);
169-
}
170-
}
171-
}
172-
173-
impl Drop for BackingFile {
174-
fn drop(&mut self) {
175-
self.unlink();
176-
}
177-
}
178-
179-
fn deleted_path(path: &Path) -> PathBuf {
180-
path.with_extension(format!("deleted-{}", Uuid::new_v4()))
133+
fn create_backing_file(path: &Path) -> io::Result<File> {
134+
OpenOptions::new()
135+
.read(true)
136+
.write(true)
137+
.create_new(true)
138+
.share_mode(sys::SHARE_ALL)
139+
.attributes(sys::TEMPORARY)
140+
.custom_flags(sys::DELETE_ON_CLOSE)
141+
.open(path)
181142
}
182143

183-
impl Drop for Shm {
184-
fn drop(&mut self) {
185-
// Remove the public backing-file name before the mapping handle drops,
186-
// preventing opens that begin after owner teardown.
187-
self.backing_file.unlink();
188-
}
144+
fn initialize_backing_file(file: &File, size: u64) -> io::Result<()> {
145+
sys::set_sparse(file)?;
146+
file.set_len(size)
189147
}
190148

191149
#[expect(clippy::len_without_is_empty, reason = "shared-memory mappings are always non-empty")]
@@ -240,7 +198,7 @@ mod tests {
240198
}
241199

242200
#[test]
243-
fn malformed_ids_and_size_mismatches_are_rejected() {
201+
fn malformed_ids_and_invalid_sizes_are_rejected() {
244202
for id in [
245203
"vite-task-fspy-not-local",
246204
r"Local\vite-task-fspy-not-a-uuid",
@@ -252,8 +210,7 @@ mod tests {
252210

253211
let owner = create(SIZE).unwrap().shm;
254212
assert_eq!(open(owner.id(), 0).err().unwrap().kind(), io::ErrorKind::InvalidInput);
255-
assert_eq!(open(owner.id(), SIZE / 2).err().unwrap().kind(), io::ErrorKind::InvalidData);
256-
assert_eq!(open(owner.id(), SIZE + 1).err().unwrap().kind(), io::ErrorKind::InvalidData);
213+
assert!(open(owner.id(), SIZE + 1).is_err());
257214
}
258215

259216
#[test]
@@ -262,10 +219,10 @@ mod tests {
262219
let collision_name = format!("{NAME_PREFIX}{}", Uuid::new_v4());
263220
let raw_backing_name = format!("{NAME_PREFIX}{}", Uuid::new_v4());
264221
let raw_backing_path = backing_path(&raw_backing_name).unwrap();
265-
let raw_backing = BackingFile::create(raw_backing_path).unwrap();
266-
raw_backing.file.set_len(SIZE as u64).unwrap();
222+
let raw_backing = create_backing_file(&raw_backing_path).unwrap();
223+
initialize_backing_file(&raw_backing, SIZE as u64).unwrap();
267224
let collision_mapping =
268-
match sys::create_file_mapping(&raw_backing.file, &collision_name).unwrap() {
225+
match sys::create_file_mapping(&raw_backing, &collision_name).unwrap() {
269226
CreatedMapping::Created(mapping) => mapping,
270227
CreatedMapping::AlreadyExists => panic!("random test mapping name collided"),
271228
};
@@ -282,7 +239,16 @@ mod tests {
282239
}
283240

284241
#[test]
285-
fn owner_cleanup_removes_backing_file_after_existing_views_close() {
242+
fn sender_opens_the_named_mapping_without_a_backing_file() {
243+
let owner = create(SIZE).unwrap().shm;
244+
let opened = open(owner.id(), SIZE).unwrap();
245+
246+
assert!(owner.backing_file.is_some());
247+
assert!(opened.backing_file.is_none());
248+
}
249+
250+
#[test]
251+
fn owner_cleanup_deletes_backing_file_and_preserves_existing_views() {
286252
let owner = create(SIZE).unwrap().shm;
287253
let id = owner.id().to_owned();
288254
let path = backing_path(&id).unwrap();
@@ -292,12 +258,12 @@ mod tests {
292258
drop(owner);
293259

294260
assert!(!path.exists());
295-
assert!(open(&id, SIZE).is_err());
296261
// SAFETY: The mapping remains live and no other test access is concurrent.
297262
unsafe { opened.as_ptr().write(17) };
298263
// SAFETY: The preceding write is complete and the mapping remains live.
299264
assert_eq!(unsafe { opened.as_ptr().read() }, 17);
300265
drop(opened);
266+
assert!(open(&id, SIZE).is_err());
301267

302268
let suffix = id.strip_prefix(NAME_PREFIX).unwrap();
303269
assert!(
@@ -309,12 +275,34 @@ mod tests {
309275
);
310276
}
311277

278+
#[test]
279+
fn sparse_failure_does_not_extend_the_file() {
280+
fs::create_dir_all(backing_dir()).unwrap();
281+
let id = format!("{NAME_PREFIX}{}", Uuid::new_v4());
282+
let path = backing_path(&id).unwrap();
283+
File::create(&path).unwrap();
284+
let read_only =
285+
OpenOptions::new().read(true).share_mode(sys::SHARE_ALL).open(&path).unwrap();
286+
287+
assert!(initialize_backing_file(&read_only, 4 * 1024 * 1024 * 1024).is_err());
288+
assert_eq!(read_only.metadata().unwrap().len(), 0);
289+
290+
drop(read_only);
291+
fs::remove_file(path).unwrap();
292+
}
293+
312294
#[cfg(target_pointer_width = "64")]
313295
#[test]
314-
fn four_gib_mapping_supports_endpoint_access() {
296+
fn four_gib_mapping_is_sparse_and_supports_endpoint_access() {
315297
const PRODUCTION_SIZE: usize = 4 * 1024 * 1024 * 1024;
298+
const MAX_ENDPOINT_ALLOCATION: u64 = 16 * 1024 * 1024;
316299

317300
let owner = create(PRODUCTION_SIZE).unwrap().shm;
301+
let backing_file = owner.backing_file.as_ref().unwrap();
302+
let (logical_size, initial_allocation) = sys::file_sizes(backing_file).unwrap();
303+
assert_eq!(logical_size, PRODUCTION_SIZE as u64);
304+
assert!(initial_allocation < MAX_ENDPOINT_ALLOCATION);
305+
318306
let opened = open(owner.id(), PRODUCTION_SIZE).unwrap();
319307
// SAFETY: Both endpoint indexes are within the exact mapped length and
320308
// accesses are synchronized within this test.
@@ -324,5 +312,9 @@ mod tests {
324312
assert_eq!(opened.as_ptr().read(), 17);
325313
assert_eq!(opened.as_ptr().add(PRODUCTION_SIZE - 1).read(), 29);
326314
}
315+
316+
let (logical_size, endpoint_allocation) = sys::file_sizes(backing_file).unwrap();
317+
assert_eq!(logical_size, PRODUCTION_SIZE as u64);
318+
assert!(endpoint_allocation < MAX_ENDPOINT_ALLOCATION);
327319
}
328320
}

0 commit comments

Comments
 (0)