| ... | ... | @@ -346,7 +346,7 @@ impl Collector { |
| 346 | 346 | self.system.refresh_processes_specifics( |
| 347 | 347 | ProcessesToUpdate::All, |
| 348 | 348 | true, |
| 349 | | process_refresh_kind(), |
| 349 | process_discovery_refresh_kind(), |
| 350 | 350 | ); |
| 351 | 351 | self.cpu_baseline_refreshed_at = Some(Instant::now()); |
| 352 | 352 | } |
| ... | ... | @@ -361,7 +361,7 @@ impl Collector { |
| 361 | 361 | self.system.refresh_processes_specifics( |
| 362 | 362 | ProcessesToUpdate::All, |
| 363 | 363 | true, |
| 364 | | process_refresh_kind(), |
| 364 | process_discovery_refresh_kind(), |
| 365 | 365 | ); |
| 366 | 366 | self.cpu_baseline_refreshed_at = Some(Instant::now()); |
| 367 | 367 | |
| ... | ... | @@ -397,15 +397,57 @@ impl Collector { |
| 397 | 397 | roots.insert(config.root_pid); |
| 398 | 398 | let tracked = select_tracked_pids(&rows, &roots); |
| 399 | 399 | let tracked_process_count = tracked.len(); |
| 400 | let process_details = if cfg!(target_os = "linux") && !tracked.is_empty() { |
| 401 | let monitor_pid = Pid::from_u32(std::process::id()); |
| 402 | let mut detail_pids = tracked |
| 403 | .iter() |
| 404 | .copied() |
| 405 | .map(Pid::from_u32) |
| 406 | .collect::<Vec<_>>(); |
| 407 | if !tracked.contains(&monitor_pid.as_u32()) { |
| 408 | detail_pids.push(monitor_pid); |
| 409 | } |
| 410 | // Detail fields need no baseline. Drop command data and OS handles after each sample. |
| 411 | let mut details = System::new(); |
| 412 | details.refresh_processes_specifics( |
| 413 | ProcessesToUpdate::Some(&detail_pids), |
| 414 | true, |
| 415 | process_refresh_kind().without_cpu(), |
| 416 | ); |
| 417 | // This process cannot be replaced during collection. Its start time |
| 418 | // exposes any boot-epoch shift between the two System instances. |
| 419 | let start_time_offset = self.system.process(monitor_pid).and_then(|process| { |
| 420 | details.process(monitor_pid).map(|detail| { |
| 421 | i128::from(detail.start_time()) - i128::from(process.start_time()) |
| 422 | }) |
| 423 | }); |
| 424 | Some((details, start_time_offset)) |
| 425 | } else { |
| 426 | None |
| 427 | }; |
| 428 | let sample_details = process_details |
| 429 | .as_ref() |
| 430 | .map_or(&self.system, |(details, _)| details); |
| 431 | let start_time_offset = process_details |
| 432 | .as_ref() |
| 433 | .map_or(Some(0), |(_, offset)| *offset); |
| 400 | 434 | let mut processes = tracked |
| 401 | 435 | .into_iter() |
| 402 | 436 | .filter_map(|pid| { |
| 403 | 437 | let process = self.system.process(Pid::from_u32(pid))?; |
| 404 | | let disk_usage = process.disk_usage(); |
| 405 | | let command = if process.cmd().is_empty() { |
| 438 | let details = sample_details.process(Pid::from_u32(pid))?; |
| 439 | if !matches_process_start_time( |
| 440 | process.start_time(), |
| 441 | details.start_time(), |
| 442 | start_time_offset?, |
| 443 | ) { |
| 444 | return None; |
| 445 | } |
| 446 | let disk_usage = details.disk_usage(); |
| 447 | let command = if details.cmd().is_empty() { |
| 406 | 448 | process.name().to_string_lossy().into_owned() |
| 407 | 449 | } else { |
| 408 | | process |
| 450 | details |
| 409 | 451 | .cmd() |
| 410 | 452 | .iter() |
| 411 | 453 | .map(|part| part.to_string_lossy()) |
| ... | ... | @@ -429,14 +471,15 @@ impl Collector { |
| 429 | 471 | ), |
| 430 | 472 | cpu_percent: process.cpu_usage(), |
| 431 | 473 | cpu_time_ms: process.accumulated_cpu_time(), |
| 432 | | resident_bytes: process.memory(), |
| 433 | | virtual_bytes: process.virtual_memory(), |
| 474 | resident_bytes: details.memory(), |
| 475 | virtual_bytes: details.virtual_memory(), |
| 434 | 476 | io_read_bytes: disk_usage.total_read_bytes, |
| 435 | 477 | io_write_bytes: disk_usage.total_written_bytes, |
| 436 | 478 | io_semantics: io_semantics(), |
| 437 | 479 | }) |
| 438 | 480 | }) |
| 439 | 481 | .collect::<Vec<_>>(); |
| 482 | drop(process_details); |
| 440 | 483 | processes.sort_by_key(|process| process.pid); |
| 441 | 484 | self.sequence = self.sequence.saturating_add(1); |
| 442 | 485 | |
| ... | ... | @@ -459,6 +502,15 @@ impl Collector { |
| 459 | 502 | } |
| 460 | 503 | } |
| 461 | 504 | |
| 505 | // Keep CPU baselines separate. Even a metadata refresh resets Linux process times. |
| 506 | fn process_discovery_refresh_kind() -> ProcessRefreshKind { |
| 507 | if cfg!(target_os = "linux") { |
| 508 | ProcessRefreshKind::nothing().with_cpu().without_tasks() |
| 509 | } else { |
| 510 | process_refresh_kind() |
| 511 | } |
| 512 | } |
| 513 | |
| 462 | 514 | fn process_refresh_kind() -> ProcessRefreshKind { |
| 463 | 515 | ProcessRefreshKind::nothing() |
| 464 | 516 | .with_memory() |
| ... | ... | @@ -472,6 +524,10 @@ fn inaccessible_process_count(selected: usize, materialized: usize) -> usize { |
| 472 | 524 | selected.saturating_sub(materialized) |
| 473 | 525 | } |
| 474 | 526 | |
| 527 | fn matches_process_start_time(discovered: u64, detail: u64, epoch_offset: i128) -> bool { |
| 528 | i128::from(detail) - epoch_offset == i128::from(discovered) |
| 529 | } |
| 530 | |
| 475 | 531 | fn remaining_cpu_measurement_delay( |
| 476 | 532 | baseline_refreshed_at: Option<Instant>, |
| 477 | 533 | now: Instant, |
| ... | ... | @@ -889,6 +945,39 @@ mod tests { |
| 889 | 945 | assert!(!matches_external_identity(10_000, Some(9_999))); |
| 890 | 946 | } |
| 891 | 947 | |
| 948 | #[test] |
| 949 | fn loads_details_when_an_existing_process_becomes_selected() { |
| 950 | let mut collector = Collector::new(); |
| 951 | let mut config = CollectorConfig { |
| 952 | root_pid: u32::MAX, |
| 953 | sample_interval: None, |
| 954 | external_processes: HashMap::new(), |
| 955 | }; |
| 956 | assert!(collector.sample(&config, None).processes.is_empty()); |
| 957 | |
| 958 | config.root_pid = std::process::id(); |
| 959 | let snapshot = collector.sample(&config, None); |
| 960 | let process = snapshot |
| 961 | .processes |
| 962 | .iter() |
| 963 | .find(|process| process.pid == config.root_pid) |
| 964 | .expect("selected process"); |
| 965 | |
| 966 | assert!(!process.command.is_empty()); |
| 967 | assert!(process.resident_bytes > 0); |
| 968 | assert!(process.cpu_percent.is_finite()); |
| 969 | } |
| 970 | |
| 971 | #[test] |
| 972 | fn accepts_clock_shifts_without_accepting_reused_process_starts() { |
| 973 | assert!(matches_process_start_time(10_000, 13_600, 3_600)); |
| 974 | assert!(!matches_process_start_time(10_000, 13_601, 3_600)); |
| 975 | assert!(matches_process_start_time(10_000, 6_400, -3_600)); |
| 976 | assert!(!matches_process_start_time(10_000, 6_401, -3_600)); |
| 977 | assert!(matches_process_start_time(10_000, 10_000, 0)); |
| 978 | assert!(!matches_process_start_time(10_000, 10_001, 0)); |
| 979 | } |
| 980 | |
| 892 | 981 | #[test] |
| 893 | 982 | fn decodes_protocol_commands() { |
| 894 | 983 | let configure = serde_json::from_str::<Command>( |