Skip to content

Commit daf99c3

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 39b502d commit daf99c3

5 files changed

Lines changed: 187 additions & 98 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 where supported, 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
@@ -233,7 +233,9 @@ mod tests {
233233
write_byte(&owner, 0, 17);
234234
drop(owner);
235235

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

crates/fspy_shm/src/windows/mod.rs

Lines changed: 111 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,16 @@ const BACKING_DIR: &str = "vite-task-fspy";
2525
/// An owned Windows shared-memory mapping.
2626
pub struct Shm {
2727
id: String,
28+
// Field order is significant: unmap the view, close the mapping, then close
29+
// the delete-on-close backing file.
2830
view: MappedView,
29-
_mapping: OwnedHandle,
30-
backing_file: BackingFile,
31+
_mapping: Option<OwnedHandle>,
32+
// Owner mappings keep this file alive until their view and mapping handle drop.
33+
#[cfg_attr(
34+
not(test),
35+
expect(dead_code, reason = "the file is retained only for RAII cleanup")
36+
)]
37+
backing_file: Option<File>,
3138
}
3239

3340
/// A newly created shared-memory mapping.
@@ -36,7 +43,7 @@ pub struct CreatedShm {
3643
pub shm: Shm,
3744
}
3845

39-
/// Creates a file-backed named mapping of `size` bytes.
46+
/// Creates a sparse, temporary file-backed named mapping of `size` bytes.
4047
///
4148
/// # Errors
4249
///
@@ -60,14 +67,14 @@ fn create_with(size: usize, mut next_name: impl FnMut() -> String) -> io::Result
6067
fn create_named(backing_dir: &Path, mapping_name: &str, size: u64) -> io::Result<Option<Shm>> {
6168
let path = backing_path(backing_dir, mapping_name)?;
6269
let id = encode_id(&path, mapping_name)?;
63-
let backing_file = match BackingFile::create(path) {
70+
let backing_file = match create_backing_file(&path) {
6471
Ok(file) => file,
6572
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => return Ok(None),
6673
Err(error) => return Err(error),
6774
};
68-
backing_file.file.set_len(size)?;
75+
initialize_backing_file(&backing_file, size)?;
6976

70-
let mapping = match sys::create_file_mapping(&backing_file.file, mapping_name)? {
77+
let mapping = match sys::create_file_mapping(&backing_file, mapping_name)? {
7178
CreatedMapping::Created(mapping) => mapping,
7279
CreatedMapping::AlreadyExists => return Ok(None),
7380
};
@@ -76,29 +83,23 @@ fn create_named(backing_dir: &Path, mapping_name: &str, size: u64) -> io::Result
7683
})?;
7784
let view = MappedView::new(&mapping, len)?;
7885

79-
Ok(Some(Shm { id, view, _mapping: mapping, backing_file }))
86+
Ok(Some(Shm { id, view, _mapping: Some(mapping), backing_file: Some(backing_file) }))
8087
}
8188

8289
/// Opens the named mapping identified by `id`.
8390
///
8491
/// # Errors
8592
///
86-
/// Returns an error if the identifier is invalid, the owner has torn down the
87-
/// mapping, or its backing file has a different size.
93+
/// Returns an error if the identifier is invalid, the mapping is unavailable,
94+
/// or `size` cannot be mapped from it.
8895
pub fn open(id: &str, size: usize) -> io::Result<Shm> {
89-
let expected_size = valid_size(size)?;
90-
let DecodedId { backing_path, mapping_name } = decode_id(id)?;
91-
let backing_file = BackingFile::open(backing_path)?;
92-
if backing_file.file.metadata()?.len() != expected_size {
93-
return Err(io::Error::new(
94-
io::ErrorKind::InvalidData,
95-
"shared-memory backing file has an unexpected size",
96-
));
97-
}
96+
valid_size(size)?;
97+
let DecodedId { mapping_name, .. } = decode_id(id)?;
9898

9999
let mapping = sys::open_file_mapping(&mapping_name)?;
100100
let view = MappedView::new(&mapping, size)?;
101-
Ok(Shm { id: id.to_owned(), view, _mapping: mapping, backing_file })
101+
drop(mapping);
102+
Ok(Shm { id: id.to_owned(), view, _mapping: None, backing_file: None })
102103
}
103104

104105
fn valid_size(size: usize) -> io::Result<u64> {
@@ -144,6 +145,7 @@ fn encode_id(backing_path: &Path, mapping_name: &str) -> io::Result<String> {
144145
}
145146

146147
struct DecodedId {
148+
#[cfg(test)]
147149
backing_path: PathBuf,
148150
mapping_name: String,
149151
}
@@ -172,7 +174,11 @@ fn decode_id(id: &str) -> io::Result<DecodedId> {
172174
let mapping_name =
173175
String::from_utf8(decode_id_part(encoded_mapping_name)?).map_err(|_| invalid_id())?;
174176
validate_id_parts(&backing_path, &mapping_name)?;
175-
Ok(DecodedId { backing_path, mapping_name })
177+
Ok(DecodedId {
178+
#[cfg(test)]
179+
backing_path,
180+
mapping_name,
181+
})
176182
}
177183

178184
fn decode_id_part(encoded: &str) -> io::Result<Vec<u8>> {
@@ -197,71 +203,32 @@ fn invalid_id() -> io::Error {
197203
io::Error::new(io::ErrorKind::InvalidInput, "invalid Windows shared-memory identifier")
198204
}
199205

200-
struct BackingFile {
201-
file: File,
202-
owner_path: Option<PathBuf>,
206+
fn create_backing_file(path: &Path) -> io::Result<File> {
207+
OpenOptions::new()
208+
.read(true)
209+
.write(true)
210+
.create_new(true)
211+
.share_mode(sys::SHARE_ALL)
212+
.attributes(sys::TEMPORARY)
213+
.custom_flags(sys::DELETE_ON_CLOSE)
214+
.open(path)
203215
}
204216

205-
impl BackingFile {
206-
fn create(path: PathBuf) -> io::Result<Self> {
207-
let file = OpenOptions::new()
208-
.read(true)
209-
.write(true)
210-
.create_new(true)
211-
.share_mode(sys::SHARE_ALL)
212-
.attributes(sys::TEMPORARY)
213-
.open(&path)?;
214-
Ok(Self { file, owner_path: Some(path) })
215-
}
216-
217-
fn open(path: PathBuf) -> io::Result<Self> {
218-
let file = OpenOptions::new()
219-
.read(true)
220-
.write(true)
221-
.share_mode(sys::SHARE_ALL)
222-
.attributes(sys::TEMPORARY)
223-
.open(path)?;
224-
Ok(Self { file, owner_path: None })
225-
}
226-
227-
fn unlink(&mut self) {
228-
let Some(path) = self.owner_path.take() else {
229-
return;
230-
};
231-
232-
let deletion_file = OpenOptions::new()
233-
.access_mode(sys::DELETE_ACCESS)
234-
.share_mode(sys::SHARE_ALL)
235-
.attributes(sys::DELETE_ON_CLOSE)
236-
.open(&path);
237-
if let Ok(deletion_file) = deletion_file {
238-
let deleted_path = deleted_path(&path);
239-
if fs::rename(&path, deleted_path).is_err() {
240-
let _ = fs::remove_file(&path);
241-
}
242-
drop(deletion_file);
243-
} else {
244-
let _ = fs::remove_file(path);
245-
}
246-
}
217+
fn initialize_backing_file(file: &File, size: u64) -> io::Result<()> {
218+
initialize_backing_file_with(file, size, sys::set_sparse)
247219
}
248220

249-
impl Drop for BackingFile {
250-
fn drop(&mut self) {
251-
self.unlink();
252-
}
253-
}
254-
255-
fn deleted_path(path: &Path) -> PathBuf {
256-
path.with_extension(format!("deleted-{}", Uuid::new_v4()))
257-
}
258-
259-
impl Drop for Shm {
260-
fn drop(&mut self) {
261-
// Remove the public backing-file name before the mapping handle drops,
262-
// preventing opens that begin after owner teardown.
263-
self.backing_file.unlink();
221+
fn initialize_backing_file_with(
222+
file: &File,
223+
size: u64,
224+
set_sparse: impl FnOnce(&File) -> io::Result<()>,
225+
) -> io::Result<()> {
226+
if let Err(error) = set_sparse(file)
227+
&& !sys::is_sparse_unsupported(&error)
228+
{
229+
return Err(error);
264230
}
231+
file.set_len(size)
265232
}
266233

267234
#[expect(clippy::len_without_is_empty, reason = "shared-memory mappings are always non-empty")]
@@ -303,6 +270,9 @@ mod tests {
303270
use std::{ffi::OsString, fs, process::Command};
304271

305272
use subprocess_test::command_for_fn;
273+
use windows_sys::Win32::Foundation::{
274+
ERROR_ACCESS_DENIED, ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED,
275+
};
306276

307277
use super::*;
308278

@@ -338,7 +308,7 @@ mod tests {
338308
}
339309

340310
#[test]
341-
fn malformed_ids_and_size_mismatches_are_rejected() {
311+
fn malformed_ids_and_invalid_sizes_are_rejected() {
342312
let owner = create(SIZE).unwrap().shm;
343313
let decoded = decode_id(owner.id()).unwrap();
344314
let encoded_path = URL_SAFE_NO_PAD.encode(
@@ -384,8 +354,7 @@ mod tests {
384354
}
385355

386356
assert_eq!(open(owner.id(), 0).err().unwrap().kind(), io::ErrorKind::InvalidInput);
387-
assert_eq!(open(owner.id(), SIZE / 2).err().unwrap().kind(), io::ErrorKind::InvalidData);
388-
assert_eq!(open(owner.id(), SIZE + 1).err().unwrap().kind(), io::ErrorKind::InvalidData);
357+
assert!(open(owner.id(), SIZE + 1).is_err());
389358
}
390359

391360
#[test]
@@ -394,10 +363,10 @@ mod tests {
394363
let collision_name = format!("{MAPPING_NAME_PREFIX}{}", Uuid::new_v4());
395364
let raw_backing_name = format!("{MAPPING_NAME_PREFIX}{}", Uuid::new_v4());
396365
let raw_backing_path = backing_path(&backing_dir, &raw_backing_name).unwrap();
397-
let raw_backing = BackingFile::create(raw_backing_path).unwrap();
398-
raw_backing.file.set_len(SIZE as u64).unwrap();
366+
let raw_backing = create_backing_file(&raw_backing_path).unwrap();
367+
initialize_backing_file(&raw_backing, SIZE as u64).unwrap();
399368
let collision_mapping =
400-
match sys::create_file_mapping(&raw_backing.file, &collision_name).unwrap() {
369+
match sys::create_file_mapping(&raw_backing, &collision_name).unwrap() {
401370
CreatedMapping::Created(mapping) => mapping,
402371
CreatedMapping::AlreadyExists => panic!("random test mapping name collided"),
403372
};
@@ -449,7 +418,16 @@ mod tests {
449418
}
450419

451420
#[test]
452-
fn owner_cleanup_removes_backing_file_after_existing_views_close() {
421+
fn sender_opens_the_named_mapping_without_a_backing_file() {
422+
let owner = create(SIZE).unwrap().shm;
423+
let opened = open(owner.id(), SIZE).unwrap();
424+
425+
assert!(owner.backing_file.is_some());
426+
assert!(opened.backing_file.is_none());
427+
}
428+
429+
#[test]
430+
fn owner_cleanup_deletes_backing_file_and_preserves_existing_views() {
453431
let owner = create(SIZE).unwrap().shm;
454432
let id = owner.id().to_owned();
455433
let DecodedId { backing_path: path, mapping_name } = decode_id(&id).unwrap();
@@ -461,12 +439,12 @@ mod tests {
461439
drop(owner);
462440

463441
assert!(!path.exists());
464-
assert!(open(&id, SIZE).is_err());
465442
// SAFETY: The mapping remains live and no other test access is concurrent.
466443
unsafe { opened.as_ptr().write(17) };
467444
// SAFETY: The preceding write is complete and the mapping remains live.
468445
assert_eq!(unsafe { opened.as_ptr().read() }, 17);
469446
drop(opened);
447+
assert!(open(&id, SIZE).is_err());
470448

471449
assert!(
472450
fs::read_dir(backing_dir).unwrap().all(|entry| !entry
@@ -477,12 +455,45 @@ mod tests {
477455
);
478456
}
479457

458+
#[test]
459+
fn unsupported_sparse_errors_fall_back_to_extending_the_file() {
460+
for code in [ERROR_INVALID_FUNCTION, ERROR_NOT_SUPPORTED] {
461+
let file = test_backing_file();
462+
initialize_backing_file_with(&file, SIZE as u64, |_| {
463+
Err(io::Error::from_raw_os_error(code.cast_signed()))
464+
})
465+
.unwrap();
466+
467+
assert_eq!(file.metadata().unwrap().len(), SIZE as u64);
468+
}
469+
}
470+
471+
#[test]
472+
fn other_sparse_errors_do_not_extend_the_file() {
473+
for code in [ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER] {
474+
let file = test_backing_file();
475+
let error = initialize_backing_file_with(&file, SIZE as u64, |_| {
476+
Err(io::Error::from_raw_os_error(code.cast_signed()))
477+
})
478+
.unwrap_err();
479+
480+
assert_eq!(error.raw_os_error(), Some(code.cast_signed()));
481+
assert_eq!(file.metadata().unwrap().len(), 0);
482+
}
483+
}
484+
480485
#[cfg(target_pointer_width = "64")]
481486
#[test]
482-
fn four_gib_mapping_supports_endpoint_access() {
487+
fn four_gib_mapping_is_sparse_and_supports_endpoint_access() {
483488
const PRODUCTION_SIZE: usize = 4 * 1024 * 1024 * 1024;
489+
const MAX_ENDPOINT_ALLOCATION: u64 = 16 * 1024 * 1024;
484490

485491
let owner = create(PRODUCTION_SIZE).unwrap().shm;
492+
let backing_file = owner.backing_file.as_ref().unwrap();
493+
let (logical_size, initial_allocation) = sys::file_sizes(backing_file).unwrap();
494+
assert_eq!(logical_size, PRODUCTION_SIZE as u64);
495+
assert!(initial_allocation < MAX_ENDPOINT_ALLOCATION);
496+
486497
let opened = open(owner.id(), PRODUCTION_SIZE).unwrap();
487498
// SAFETY: Both endpoint indexes are within the exact mapped length and
488499
// accesses are synchronized within this test.
@@ -492,5 +503,16 @@ mod tests {
492503
assert_eq!(opened.as_ptr().read(), 17);
493504
assert_eq!(opened.as_ptr().add(PRODUCTION_SIZE - 1).read(), 29);
494505
}
506+
507+
let (logical_size, endpoint_allocation) = sys::file_sizes(backing_file).unwrap();
508+
assert_eq!(logical_size, PRODUCTION_SIZE as u64);
509+
assert!(endpoint_allocation < MAX_ENDPOINT_ALLOCATION);
510+
}
511+
512+
fn test_backing_file() -> File {
513+
let backing_dir = create_backing_dir().unwrap();
514+
let mapping_name = format!("{MAPPING_NAME_PREFIX}{}", Uuid::new_v4());
515+
let path = backing_path(&backing_dir, &mapping_name).unwrap();
516+
create_backing_file(&path).unwrap()
495517
}
496518
}

0 commit comments

Comments
 (0)