#[cfg(windows)]
mod windows_phases {
use std::io;
use std::os::windows::io::AsRawHandle as _;
use std::os::windows::process::CommandExt as _;
use std::process::{Child, Stdio};
use std::sync::Mutex;
use std::time::Duration;
use criterion::Criterion;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject,
};
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
use windows_sys::Win32::System::Threading::{
CREATE_SUSPENDED, OpenThread, ResumeThread, THREAD_SUSPEND_RESUME,
};
const FAN_OUT: usize = 16;
fn fixture_child() -> std::process::Command {
let comspec = std::env::var_os("ComSpec")
.unwrap_or_else(|| std::ffi::OsString::from(r"C:\Windows\System32\cmd.exe"));
let mut cmd = std::process::Command::new(comspec);
cmd.args(["/c", "exit"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd
}
#[derive(Debug)]
struct OwnedJob(HANDLE);
impl OwnedJob {
fn create() -> io::Result<Self> {
let handle = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
if handle.is_null() {
return Err(io::Error::last_os_error());
}
let job = Self(handle);
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let ok = unsafe {
SetInformationJobObject(
job.0,
JobObjectExtendedLimitInformation,
std::ptr::from_ref(&info).cast(),
u32::try_from(std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>())
.expect("the extended-limit struct fits in a u32"),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(job)
}
fn assign(&self, child: &Child) -> io::Result<()> {
let ok = unsafe { AssignProcessToJobObject(self.0, child.as_raw_handle() as HANDLE) };
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
}
impl Drop for OwnedJob {
fn drop(&mut self) {
unsafe { CloseHandle(self.0) };
}
}
fn resume_process_threads(pid: u32) -> io::Result<()> {
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snapshot == INVALID_HANDLE_VALUE {
return Err(io::Error::last_os_error());
}
let mut entry: THREADENTRY32 = unsafe { std::mem::zeroed() };
entry.dwSize = u32::try_from(std::mem::size_of::<THREADENTRY32>())
.expect("the thread entry fits in a u32");
let mut resumed = 0u32;
let mut ok = unsafe { Thread32First(snapshot, &mut entry) };
while ok != 0 {
if entry.th32OwnerProcessID == pid {
let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
if !thread.is_null() {
if unsafe { ResumeThread(thread) } != u32::MAX {
resumed += 1;
}
unsafe { CloseHandle(thread) };
}
}
ok = unsafe { Thread32Next(snapshot, &mut entry) };
}
unsafe { CloseHandle(snapshot) };
if resumed == 0 {
return Err(io::Error::other("no thread resumed"));
}
Ok(())
}
type NtGetNextThread = unsafe extern "system" fn(
process: HANDLE,
thread: HANDLE,
desired_access: u32,
handle_attributes: u32,
flags: u32,
new_thread: *mut HANDLE,
) -> i32;
fn nt_get_next_thread() -> Option<NtGetNextThread> {
const NTDLL_UTF16: &[u16] = &[
b'n' as u16,
b't' as u16,
b'd' as u16,
b'l' as u16,
b'l' as u16,
b'.' as u16,
b'd' as u16,
b'l' as u16,
b'l' as u16,
0,
];
let ntdll = unsafe { GetModuleHandleW(NTDLL_UTF16.as_ptr()) };
if ntdll.is_null() {
return None;
}
let symbol =
unsafe { GetProcAddress(ntdll, c"NtGetNextThread".to_bytes_with_nul().as_ptr()) };
symbol.map(|symbol| unsafe {
std::mem::transmute::<unsafe extern "system" fn() -> isize, NtGetNextThread>(symbol)
})
}
fn resume_process_threads_direct(process: HANDLE) -> io::Result<()> {
let next_thread = nt_get_next_thread()
.ok_or_else(|| io::Error::other("ntdll!NtGetNextThread is unavailable"))?;
let mut resumed = 0u32;
let mut cursor: HANDLE = std::ptr::null_mut();
loop {
let mut thread: HANDLE = std::ptr::null_mut();
let status =
unsafe { next_thread(process, cursor, THREAD_SUSPEND_RESUME, 0, 0, &mut thread) };
if !cursor.is_null() {
unsafe { CloseHandle(cursor) };
}
if status < 0 || thread.is_null() {
break;
}
if unsafe { ResumeThread(thread) } != u32::MAX {
resumed += 1;
}
cursor = thread;
}
if resumed == 0 {
return Err(io::Error::other("no thread resumed"));
}
Ok(())
}
fn fixture_long_lived_suspended_child() -> Child {
fixture_child()
.creation_flags(CREATE_SUSPENDED)
.spawn()
.expect("spawn the long-lived suspended fixture child")
}
fn count_threads_direct(next_thread: NtGetNextThread, process: HANDLE) -> u32 {
let mut found = 0u32;
let mut cursor: HANDLE = std::ptr::null_mut();
loop {
let mut thread: HANDLE = std::ptr::null_mut();
let status =
unsafe { next_thread(process, cursor, THREAD_SUSPEND_RESUME, 0, 0, &mut thread) };
if !cursor.is_null() {
unsafe { CloseHandle(cursor) };
}
if status < 0 || thread.is_null() {
break;
}
found += 1;
cursor = thread;
}
found
}
fn thread_snapshot_walk(pid: u32) -> u32 {
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if snapshot == INVALID_HANDLE_VALUE {
return 0;
}
let mut entry: THREADENTRY32 = unsafe { std::mem::zeroed() };
entry.dwSize = u32::try_from(std::mem::size_of::<THREADENTRY32>())
.expect("the thread entry fits in a u32");
let mut found = 0u32;
let mut ok = unsafe { Thread32First(snapshot, &mut entry) };
while ok != 0 {
if entry.th32OwnerProcessID == pid {
found += 1;
}
ok = unsafe { Thread32Next(snapshot, &mut entry) };
}
unsafe { CloseHandle(snapshot) };
found
}
pub fn bench_start_phases(c: &mut Criterion) {
let mut group = c.benchmark_group("windows_start_phases");
group.bench_function("os_spawn_plain", |b| {
b.iter(|| {
let mut child = fixture_child().spawn().expect("spawn the fixture child");
child.wait().expect("wait for the fixture child");
});
});
group.bench_function("os_spawn_suspended_resume_snapshot", |b| {
b.iter(|| {
let mut child = fixture_child()
.creation_flags(CREATE_SUSPENDED)
.spawn()
.expect("spawn the suspended fixture child");
resume_process_threads(child.id()).expect("resume the fixture child");
child.wait().expect("wait for the fixture child");
});
});
group.bench_function("os_spawn_suspended_resume_direct", |b| {
b.iter(|| {
let mut child = fixture_child()
.creation_flags(CREATE_SUSPENDED)
.spawn()
.expect("spawn the suspended fixture child");
resume_process_threads_direct(child.as_raw_handle() as HANDLE)
.expect("resume the fixture child");
child.wait().expect("wait for the fixture child");
});
});
group.bench_function("containment_sequence_snapshot", |b| {
b.iter(|| {
let job = OwnedJob::create().expect("create the job object");
let mut child = fixture_child()
.creation_flags(CREATE_SUSPENDED)
.spawn()
.expect("spawn the suspended fixture child");
job.assign(&child).expect("assign the child to the job");
resume_process_threads(child.id()).expect("resume the fixture child");
child.wait().expect("wait for the fixture child");
});
});
group.bench_function("containment_sequence_direct", |b| {
b.iter(|| {
let job = OwnedJob::create().expect("create the job object");
let mut child = fixture_child()
.creation_flags(CREATE_SUSPENDED)
.spawn()
.expect("spawn the suspended fixture child");
job.assign(&child).expect("assign the child to the job");
resume_process_threads_direct(child.as_raw_handle() as HANDLE)
.expect("resume the fixture child");
child.wait().expect("wait for the fixture child");
});
});
group.finish();
}
pub fn bench_start_primitives(c: &mut Criterion) {
let mut group = c.benchmark_group("windows_start_primitives");
group.bench_function("job_object_create", |b| {
b.iter(|| {
let job = OwnedJob::create().expect("create the job object");
std::hint::black_box(&job);
});
});
group.bench_function("thread_snapshot_walk", |b| {
let own_pid = std::process::id();
b.iter(|| {
std::hint::black_box(thread_snapshot_walk(own_pid));
});
});
group.bench_function("direct_thread_walk", |b| {
let job = OwnedJob::create().expect("create the job object");
let mut child = fixture_long_lived_suspended_child();
job.assign(&child)
.expect("contain the suspended fixture child");
let handle = child.as_raw_handle() as HANDLE;
let next_thread = nt_get_next_thread().expect("ntdll!NtGetNextThread is available");
b.iter(|| {
std::hint::black_box(count_threads_direct(next_thread, handle));
});
child.kill().expect("kill the suspended fixture child");
child.wait().expect("reap the suspended fixture child");
drop(job);
});
group.bench_function("program_lookup_bare_name", |b| {
b.iter(|| {
let found = processkit::Command::new("cmd")
.resolve_program()
.expect("resolve the bare command name");
std::hint::black_box(found);
});
});
group.bench_function("program_lookup_absolute_path", |b| {
let absolute = processkit::Command::new("cmd")
.resolve_program()
.expect("resolve the bare command name once, outside the timed section");
b.iter(|| {
let found = processkit::Command::new(&absolute)
.resolve_program()
.expect("resolve the absolute program path");
std::hint::black_box(found);
});
});
group.finish();
}
pub fn bench_spawn_serialization(c: &mut Criterion) {
let mut group = c.benchmark_group("windows_spawn_serialization");
group.bench_function("fan_out16_parallel", |b| {
b.iter(|| {
std::thread::scope(|scope| {
for _ in 0..FAN_OUT {
scope.spawn(|| {
let mut child =
fixture_child().spawn().expect("spawn the fixture child");
child.wait().expect("wait for the fixture child");
});
}
});
});
});
group.bench_function("fan_out16_serialized_spawn", |b| {
let lock = Mutex::new(());
b.iter(|| {
std::thread::scope(|scope| {
for _ in 0..FAN_OUT {
scope.spawn(|| {
let mut child = {
let _guard = lock.lock().expect("the spawn lock is never poisoned");
fixture_child().spawn().expect("spawn the fixture child")
};
child.wait().expect("wait for the fixture child");
});
}
});
});
});
group.finish();
}
pub fn configure() -> Criterion {
Criterion::default()
.sample_size(50)
.warm_up_time(Duration::from_secs(15))
.measurement_time(Duration::from_secs(20))
}
}
#[cfg(windows)]
criterion::criterion_group! {
name = windows_start_benches;
config = windows_phases::configure();
targets =
windows_phases::bench_start_phases,
windows_phases::bench_start_primitives,
windows_phases::bench_spawn_serialization
}
#[cfg(windows)]
criterion::criterion_main!(windows_start_benches);
#[cfg(not(windows))]
fn main() {
println!(
"win_spawn_phases measures Windows Job Object / suspend-resume / PATH-lookup start \
cost and has no meaning on this target; run benches/compare.rs instead."
);
}