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//!
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
4848use 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 ) ]
169240struct 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
505576fn 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- }
0 commit comments