Skip to content

Commit 7217ecb

Browse files
fix: make xtask check pass on Windows (#357)
1 parent bcb15bc commit 7217ecb

5 files changed

Lines changed: 166 additions & 54 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ serde_yaml = "0.9"
6363
libc = "0.2.186"
6464
time = { version = "0.3", features = ["local-offset", "formatting", "macros"] }
6565
which = "8.0.2"
66+
windows-sys = { version = "0.61.2", features = [
67+
"Win32_Foundation",
68+
"Win32_System_ProcessStatus",
69+
"Win32_System_Threading",
70+
] }
6671

6772
# Test dependencies
6873
criterion = "0.8.2"

crates/webui/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ libc = { workspace = true }
4444
microsoft-webui-handler = { path = "../webui-handler", version = "0.0.17" }
4545
microsoft-webui-protocol = { path = "../webui-protocol", version = "0.0.17" }
4646

47+
[target.'cfg(windows)'.dev-dependencies]
48+
windows-sys = { workspace = true }
49+
4750
[[bench]]
4851
name = "contact_book_bench"
4952
harness = false

crates/webui/examples/streaming_resource_bench.rs

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020
//!
2121
//! * **allocations** — count of `alloc` calls (custom GlobalAlloc)
2222
//! * **bytes allocated** — total bytes requested
23-
//! * **CPU user time** — `getrusage(RUSAGE_SELF).ru_utime` delta on Unix
24-
//! * **peak RSS** — `ru_maxrss` high-water mark on Unix
23+
//! * **CPU user time** — `getrusage(RUSAGE_SELF).ru_utime` delta on Unix,
24+
//! `GetProcessTimes` user time delta on Windows
25+
//! * **peak RSS** — `ru_maxrss` high-water mark on Unix,
26+
//! `PROCESS_MEMORY_COUNTERS.PeakWorkingSetSize` on Windows
2527
//!
2628
//! Unlike criterion (which only reports wall-clock), this gives a
2729
//! direct allocator-level view useful for verifying that the streaming
2830
//! writer's "zero per-write allocation" claim actually holds in the
29-
//! production path. CPU and RSS columns are reported as zero on platforms
30-
//! without `getrusage`.
31+
//! production path.
3132
//!
3233
//! Usage:
3334
//!
@@ -37,12 +38,11 @@
3738
3839
#![allow(missing_docs)]
3940
// SAFETY EXEMPTION: This is a benchmark example, not library code.
40-
// `GlobalAlloc` and, on Unix, `libc::getrusage` require `unsafe` blocks;
41-
// their callers here have correct contracts (forwarding to System allocator
42-
// with original layouts; `rusage` is fully zero-initialised before the
43-
// FFI call). The workspace `unsafe_code = "deny"` lint applies to
44-
// production library code; benchmarking infrastructure is exempted at
45-
// the file level with this attribute.
41+
// `GlobalAlloc` and process resource APIs require `unsafe` blocks; their
42+
// callers here have correct contracts (forwarding to System allocator with
43+
// original layouts; resource structs are fully initialised before FFI calls).
44+
// The workspace `unsafe_code = "deny"` lint applies to production library code;
45+
// benchmarking infrastructure is exempted at the file level with this attribute.
4646
#![allow(unsafe_code)]
4747

4848
use bytes::Bytes;
@@ -107,19 +107,17 @@ fn alloc_snapshot() -> (usize, usize) {
107107
)
108108
}
109109

110-
// ── Resource usage helpers ─────────────────────────────────────────────
110+
// ── Process resource helpers ───────────────────────────────────────────
111111

112112
#[derive(Copy, Clone)]
113-
struct Rusage {
113+
struct ProcessUsage {
114114
user_cpu: Duration,
115115
sys_cpu: Duration,
116-
/// Maximum resident set size, in bytes (macOS) or KB (Linux).
117-
/// Normalised by `max_rss_bytes`.
118-
#[cfg(unix)]
119-
max_rss_raw: i64,
116+
/// Maximum resident set size, normalized to bytes.
117+
max_rss_bytes: i64,
120118
}
121119

122-
impl Rusage {
120+
impl ProcessUsage {
123121
#[cfg(unix)]
124122
fn now() -> Self {
125123
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
@@ -130,31 +128,66 @@ impl Rusage {
130128
Self {
131129
user_cpu: timeval_to_duration(usage.ru_utime),
132130
sys_cpu: timeval_to_duration(usage.ru_stime),
133-
max_rss_raw: usage.ru_maxrss as i64,
131+
max_rss_bytes: unix_rss_to_bytes(usage.ru_maxrss as i64),
132+
}
133+
}
134+
135+
#[cfg(windows)]
136+
fn now() -> Self {
137+
use windows_sys::Win32::Foundation::FILETIME;
138+
use windows_sys::Win32::System::ProcessStatus::{
139+
K32GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS,
140+
};
141+
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes};
142+
143+
// GetCurrentProcess returns a pseudo-handle owned by the process; it
144+
// must not be closed.
145+
let process = unsafe { GetCurrentProcess() };
146+
let mut creation_time = FILETIME::default();
147+
let mut exit_time = FILETIME::default();
148+
let mut kernel_time = FILETIME::default();
149+
let mut user_time = FILETIME::default();
150+
// SAFETY: all pointers refer to writable FILETIME values and the
151+
// pseudo-handle returned by GetCurrentProcess is valid for this call.
152+
let times_ok = unsafe {
153+
GetProcessTimes(
154+
process,
155+
&mut creation_time,
156+
&mut exit_time,
157+
&mut kernel_time,
158+
&mut user_time,
159+
)
160+
};
161+
assert_ne!(times_ok, 0, "GetProcessTimes failed");
162+
163+
let counters_size = process_memory_counters_size();
164+
// SAFETY: zeroed PROCESS_MEMORY_COUNTERS is valid after its cb field is
165+
// set to the structure size required by GetProcessMemoryInfo.
166+
let mut counters: PROCESS_MEMORY_COUNTERS = unsafe { std::mem::zeroed() };
167+
counters.cb = counters_size;
168+
// SAFETY: counters points to writable memory with cb initialized, and
169+
// the pseudo-handle returned by GetCurrentProcess is valid for this call.
170+
let memory_ok = unsafe { K32GetProcessMemoryInfo(process, &mut counters, counters_size) };
171+
assert_ne!(memory_ok, 0, "GetProcessMemoryInfo failed");
172+
173+
Self {
174+
user_cpu: filetime_to_duration(user_time),
175+
sys_cpu: filetime_to_duration(kernel_time),
176+
max_rss_bytes: usize_to_i64_saturating(counters.PeakWorkingSetSize),
134177
}
135178
}
136179

137-
#[cfg(not(unix))]
180+
#[cfg(not(any(unix, windows)))]
138181
fn now() -> Self {
139182
Self {
140183
user_cpu: Duration::ZERO,
141184
sys_cpu: Duration::ZERO,
185+
max_rss_bytes: -1,
142186
}
143187
}
144188

145189
fn max_rss_bytes(&self) -> i64 {
146-
#[cfg(all(unix, target_os = "macos"))]
147-
{
148-
self.max_rss_raw
149-
}
150-
#[cfg(all(unix, not(target_os = "macos")))]
151-
{
152-
self.max_rss_raw * 1024
153-
}
154-
#[cfg(not(unix))]
155-
{
156-
0
157-
}
190+
self.max_rss_bytes
158191
}
159192
}
160193

@@ -165,6 +198,44 @@ fn timeval_to_duration(tv: libc::timeval) -> Duration {
165198
Duration::new(secs, usecs * 1_000)
166199
}
167200

201+
#[cfg(unix)]
202+
fn unix_rss_to_bytes(raw: i64) -> i64 {
203+
if cfg!(target_os = "macos") {
204+
raw
205+
} else {
206+
raw.saturating_mul(1024)
207+
}
208+
}
209+
210+
#[cfg(windows)]
211+
fn filetime_to_duration(filetime: windows_sys::Win32::Foundation::FILETIME) -> Duration {
212+
let ticks = (u64::from(filetime.dwHighDateTime) << 32) | u64::from(filetime.dwLowDateTime);
213+
let secs = ticks / 10_000_000;
214+
let nanos = match u32::try_from((ticks % 10_000_000) * 100) {
215+
Ok(value) => value,
216+
Err(_) => panic!("FILETIME nanosecond remainder must fit in u32"),
217+
};
218+
Duration::new(secs, nanos)
219+
}
220+
221+
#[cfg(windows)]
222+
fn usize_to_i64_saturating(value: usize) -> i64 {
223+
match i64::try_from(value) {
224+
Ok(value) => value,
225+
Err(_) => i64::MAX,
226+
}
227+
}
228+
229+
#[cfg(windows)]
230+
fn process_memory_counters_size() -> u32 {
231+
match u32::try_from(std::mem::size_of::<
232+
windows_sys::Win32::System::ProcessStatus::PROCESS_MEMORY_COUNTERS,
233+
>()) {
234+
Ok(value) => value,
235+
Err(_) => panic!("PROCESS_MEMORY_COUNTERS size must fit in u32"),
236+
}
237+
}
238+
168239
#[derive(Copy, Clone)]
169240
struct ResourceDelta {
170241
iters: usize,
@@ -434,15 +505,15 @@ where
434505
}
435506

436507
let (a0, b0) = alloc_snapshot();
437-
let r0 = Rusage::now();
508+
let r0 = ProcessUsage::now();
438509
let t0 = Instant::now();
439510

440511
for _ in 0..iters {
441512
f();
442513
}
443514

444515
let wall = t0.elapsed();
445-
let r1 = Rusage::now();
516+
let r1 = ProcessUsage::now();
446517
let (a1, b1) = alloc_snapshot();
447518

448519
ResourceDelta {
@@ -503,6 +574,9 @@ fn format_bytes_per_run(bytes: f64) -> String {
503574
}
504575

505576
fn format_total_rss(bytes: i64) -> String {
577+
if bytes < 0 {
578+
return "n/a".to_string();
579+
}
506580
if bytes < 1024 * 1024 {
507581
format!("{:.1} KiB", bytes as f64 / 1024.0)
508582
} else {
@@ -754,8 +828,8 @@ fn main() {
754828
iters_per_scale
755829
);
756830
println!(
757-
"RSS column = process-wide high-water mark on Unix; 0 on platforms \
758-
without getrusage."
831+
"RSS column = process-wide high-water mark observed at end of phase \
832+
(cumulative across all phases, only meaningful as a peak)."
759833
);
760834
print_header();
761835

@@ -809,15 +883,15 @@ fn main() {
809883
println!();
810884
println!("Notes:");
811885
println!(" * `allocs/run` and `bytes/run` are exact (custom GlobalAlloc).");
812-
if cfg!(unix) {
886+
if cfg!(windows) {
887+
println!(" * `user µs/run` is `GetProcessTimes` user time delta / iters.");
888+
println!(" * `process RSS` is `PeakWorkingSetSize` for the process at");
889+
} else {
813890
println!(" * `user µs/run` is `getrusage(RUSAGE_SELF).ru_utime` delta / iters.");
814891
println!(" * `process RSS` is the high-water mark for the whole process at");
815-
println!(" phase end. Per-iteration RSS is not directly observable; use");
816-
println!(" `bytes/run` to compare per-render heap pressure across paths.");
817-
} else {
818-
println!(" * `user µs/run`, `sys µs/run`, and `process RSS` are unavailable");
819-
println!(" on this platform and reported as 0.");
820892
}
893+
println!(" phase end. Per-iteration RSS is not directly observable; use");
894+
println!(" `bytes/run` to compare per-render heap pressure across paths.");
821895

822896
match mode {
823897
Mode::Print => {}
@@ -843,14 +917,3 @@ fn delta_to_row(label: &str, delta: ResourceDelta) -> SnapshotRow {
843917
rss_high_water_bytes: pi.rss_bytes,
844918
}
845919
}
846-
847-
#[cfg(test)]
848-
mod tests {
849-
use super::Rusage;
850-
851-
#[test]
852-
fn captures_resource_snapshot_on_current_platform() {
853-
let usage = Rusage::now();
854-
let _ = usage.max_rss_bytes();
855-
}
856-
}

xtask/src/build_examples.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,18 @@ pub fn run_integration_builds() -> Result<(), String> {
103103

104104
// ── App builds ──────────────────────────────────────────────────────────
105105

106+
fn is_example_app_dir(app_dir: &Path) -> bool {
107+
app_dir.join("package.json").is_file()
108+
}
109+
106110
pub fn run_app_builds() -> Result<(), String> {
107111
use std::thread;
108112

109113
let apps_root = Path::new("examples/app");
110-
let app_dirs = collect_child_dirs(apps_root)?;
114+
let app_dirs: Vec<_> = collect_child_dirs(apps_root)?
115+
.into_iter()
116+
.filter(|app_dir| is_example_app_dir(app_dir))
117+
.collect();
111118

112119
if app_dirs.is_empty() {
113120
eprintln!(
@@ -269,6 +276,39 @@ fn available_integrations() -> String {
269276

270277
fn available_apps() -> Result<String, String> {
271278
let dirs = collect_child_dirs(Path::new("examples/app"))?;
272-
let names: Vec<String> = dirs.iter().map(|d| display_name(d)).collect();
279+
let names: Vec<String> = dirs
280+
.iter()
281+
.filter(|d| is_example_app_dir(d))
282+
.map(|d| display_name(d))
283+
.collect();
273284
Ok(names.join(", "))
274285
}
286+
287+
#[cfg(test)]
288+
mod tests {
289+
use super::is_example_app_dir;
290+
use std::fs;
291+
292+
#[test]
293+
fn ignores_generated_directories_without_app_manifest() -> Result<(), Box<dyn std::error::Error>>
294+
{
295+
let temp = tempfile::tempdir()?;
296+
let app_dir = temp.path().join("routes-advanced");
297+
fs::create_dir(&app_dir)?;
298+
fs::create_dir(app_dir.join("dist"))?;
299+
300+
assert!(!is_example_app_dir(&app_dir));
301+
Ok(())
302+
}
303+
304+
#[test]
305+
fn treats_package_directories_as_apps() -> Result<(), Box<dyn std::error::Error>> {
306+
let temp = tempfile::tempdir()?;
307+
let app_dir = temp.path().join("calculator");
308+
fs::create_dir(&app_dir)?;
309+
fs::write(app_dir.join("package.json"), "{}\n")?;
310+
311+
assert!(is_example_app_dir(&app_dir));
312+
Ok(())
313+
}
314+
}

0 commit comments

Comments
 (0)