diff --git a/Cargo.lock b/Cargo.lock index 9179ac2..ae41384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "arc-cell" version = "0.3.3" @@ -87,6 +99,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.60" @@ -105,6 +123,58 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -130,6 +200,52 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -145,6 +261,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "darling" version = "0.21.3" @@ -186,6 +308,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "endian-type" version = "0.1.2" @@ -302,6 +430,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -317,6 +456,12 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "1.4.0" @@ -442,6 +587,26 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -609,6 +774,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -750,6 +921,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -794,6 +993,7 @@ name = "prometric" version = "0.2.2" dependencies = [ "arc-cell", + "criterion", "hyper", "hyper-util", "metrics-exporter-prometheus", @@ -929,6 +1129,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -938,6 +1158,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "ring" version = "0.17.14" @@ -1005,6 +1254,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -1043,6 +1301,49 @@ dependencies = [ "libc", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1154,6 +1455,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.52.0" @@ -1252,6 +1563,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -1347,6 +1668,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1583,3 +1913,9 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/prometric/Cargo.toml b/prometric/Cargo.toml index 8701c6b..7b822f3 100644 --- a/prometric/Cargo.toml +++ b/prometric/Cargo.toml @@ -39,3 +39,9 @@ process = ["dep:sysinfo"] summary = ["dep:metrics-util", "dep:metrics-exporter-prometheus", "dep:parking_lot", "dep:quanta", "dep:orx-concurrent-vec", "dep:arc-cell"] [dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "process_collector" +harness = false +required-features = ["process"] \ No newline at end of file diff --git a/prometric/benches/process_collector.rs b/prometric/benches/process_collector.rs new file mode 100644 index 0000000..49d0787 --- /dev/null +++ b/prometric/benches/process_collector.rs @@ -0,0 +1,197 @@ +//! Benchmarks for [`ProcessCollector`] refresh cost. +//! +//! # Purpose +//! +//! `sysinfo` refreshes can take tens of milliseconds depending on which +//! subsystems are polled. These benchmarks isolate the cost of each +//! [`RefreshKind`] component so we can document collection overhead and +//! justify any changes to the default configuration. +//! +//! # Structure +//! +//! Each benchmark function represents one [`RefreshKind`] configuration: +//! - [`cpu_only`] — floor cost, no freq polling, no tasks, no disk +//! - [`cpu_with_freq`] — isolates the overhead of CPU frequency reads +//! - [`with_disk_usage`] — isolates `/proc//io` (Linux) read cost +//! - [`with_tasks`] — isolates `/proc//task/*` enumeration cost +//! - [`current_default`] — exact config used by [`ProcessCollector`] today +//! - [`proposed_slim`] — candidate slim config, drops freq polling +//! +//! # Running +//! +//! ```bash +//! # All benchmarks, bencher output (used by CI) +//! cargo bench --bench process_collector -- --output-format bencher --noplot +//! +//! # Single benchmark with full Criterion HTML report +//! cargo bench --bench process_collector -- with_tasks +//! +//! # Quick smoke-run (1 sample, no statistical analysis) +//! cargo bench --bench process_collector -- --sample-size 10 +//! ``` + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use sysinfo::{CpuRefreshKind, MemoryRefreshKind, ProcessRefreshKind, RefreshKind, System}; + +/// Runs a single [`RefreshKind`] configuration under the `process_refresh` group. +/// +/// # Warm-up +/// +/// A single [`System::refresh_specifics`] call is made before the benchmark +/// loop starts. This mirrors [`ProcessCollector::new`], which also does one +/// eager refresh so the first [`collect`] call has a valid prior sample to +/// diff against (required for accurate CPU % calculation). +/// +/// Without the warm-up, the first iteration would measure cold-cache kernel +/// data structures rather than steady-state refresh cost. +fn bench_refresh(c: &mut Criterion, label: &str, specifics: RefreshKind) { + let mut group = c.benchmark_group("process_refresh"); + + let mut sys = System::new_with_specifics(specifics); + sys.refresh_specifics(specifics); // warm-up — mirrors ProcessCollector::new() + + group.bench_with_input(BenchmarkId::new("refresh_specifics", label), &specifics, |b, &spec| { + b.iter(|| sys.refresh_specifics(spec)) + }); + + group.finish(); +} + +/// Cheapest meaningful config: CPU usage + RAM + per-process CPU/memory. +/// +/// No frequency reads, no thread enumeration, no disk I/O stats. +/// This is the floor — the minimum needed to populate all non-optional metrics. +/// Use this as the baseline when reading results; every other config adds +/// cost on top of this number. +fn cpu_only(c: &mut Criterion) { + bench_refresh( + c, + "cpu_usage_only", + RefreshKind::nothing() + .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) + .with_memory(MemoryRefreshKind::nothing().with_ram()) + .with_processes(ProcessRefreshKind::nothing().with_cpu().with_memory()), + ); +} + +/// Adds CPU frequency reads on top of [`cpu_only`]. +/// +/// On Linux this reads `/sys/bus/cpu/devices/cpu*/cpufreq/scaling_cur_freq` +/// (one sysfs read per logical core). On macOS it goes through `sysctl`. +/// The delta between this and [`cpu_only`] is the pure frequency-polling cost. +/// +/// Frequency is used by [`ProcessCollector`] for `system_min/max_cpu_frequency` +/// gauges. If those metrics are not needed, dropping [`CpuRefreshKind::everything`] +/// for [`CpuRefreshKind::nothing().with_cpu_usage()`] is the first easy saving. +fn cpu_with_freq(c: &mut Criterion) { + bench_refresh( + c, + "cpu_with_frequency", + RefreshKind::nothing() + .with_cpu(CpuRefreshKind::everything()) // everything() = usage + frequency + .with_memory(MemoryRefreshKind::nothing().with_ram()) + .with_processes(ProcessRefreshKind::nothing().with_cpu().with_memory()), + ); +} + +/// Adds disk I/O accounting on top of [`cpu_only`]. +/// +/// On Linux this reads `/proc//io` (one syscall per tracked process). +/// On macOS it uses `proc_pidinfo` with `PROC_PIDTASKINFO`. +/// The delta between this and [`cpu_only`] is the pure disk-stat cost. +/// +/// Used by [`ProcessCollector`] for the `process_disk_written_bytes_total` counter. +fn with_disk_usage(c: &mut Criterion) { + bench_refresh( + c, + "with_disk_usage", + RefreshKind::nothing() + .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) + .with_memory(MemoryRefreshKind::nothing().with_ram()) + .with_processes( + ProcessRefreshKind::nothing().with_cpu().with_memory().with_disk_usage(), + ), + ); +} + +/// Adds thread/task enumeration on top of [`cpu_only`]. +/// +/// On Linux this walks `/proc//task/*` — cost scales with thread count. +/// On macOS `with_tasks()` is a no-op; sysinfo does not expose per-thread +/// data there, so this bench will read identically to [`cpu_only`] on macOS. +/// +/// Expected to be the most expensive component on Linux for processes with +/// many threads. The delta between this and [`cpu_only`] is the task-walk cost. +/// +/// Used by [`ProcessCollector`] for `process_threads` and the +/// `process_thread_usage` gauge vec. +fn with_tasks(c: &mut Criterion) { + bench_refresh( + c, + "with_tasks", + RefreshKind::nothing() + .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) + .with_memory(MemoryRefreshKind::nothing().with_ram()) + .with_processes(ProcessRefreshKind::nothing().with_cpu().with_memory().with_tasks()), + ); +} + +/// The exact [`RefreshKind`] configuration used by [`ProcessCollector::new`] today. +/// +/// This is the reference point for any proposed changes. All other configs +/// should be compared against this number — not against each other — so the +/// PR can state a concrete "X% reduction in collection cost". +fn current_default(c: &mut Criterion) { + bench_refresh( + c, + "current_default", + RefreshKind::nothing() + .with_cpu(CpuRefreshKind::everything()) + .with_memory(MemoryRefreshKind::nothing().with_ram()) + .with_processes( + ProcessRefreshKind::nothing() + .with_cpu() + .with_memory() + .with_disk_usage() + .with_tasks(), + ), + ); +} + +/// Proposed slim config: identical to [`current_default`] but drops frequency polling. +/// +/// Rationale: `system_min/max_cpu_frequency` gauges are rarely actionable in +/// a process-health dashboard. Dropping [`CpuRefreshKind::everything`] in favour +/// of [`CpuRefreshKind::nothing().with_cpu_usage()`] removes the per-core sysfs +/// reads while keeping all other metrics intact. +/// +/// If the delta vs [`current_default`] is meaningful (>~20% on either platform), +/// this config should become the new default and the frequency gauges should be +/// moved behind an opt-in flag. +fn proposed_slim(c: &mut Criterion) { + bench_refresh( + c, + "proposed_slim", + RefreshKind::nothing() + .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) // freq dropped + .with_memory(MemoryRefreshKind::nothing().with_ram()) + .with_processes( + ProcessRefreshKind::nothing() + .with_cpu() + .with_memory() + .with_disk_usage() + .with_tasks(), + ), + ); +} + +criterion_group!( + benches, + cpu_only, + cpu_with_freq, + with_disk_usage, + with_tasks, + current_default, + proposed_slim +); +criterion_main!(benches); diff --git a/prometric/src/process.rs b/prometric/src/process.rs index a363141..937b5c1 100644 --- a/prometric/src/process.rs +++ b/prometric/src/process.rs @@ -46,7 +46,7 @@ impl ProcessCollector { pub fn new(registry: &Registry) -> Self { // Create the stats that will be refreshed let specifics = RefreshKind::nothing() - .with_cpu(CpuRefreshKind::everything()) + .with_cpu(CpuRefreshKind::nothing().with_cpu_usage()) .with_memory(MemoryRefreshKind::nothing().with_ram()) .with_processes( ProcessRefreshKind::nothing()