| ... | @@ -346,7 +346,7 @@ impl Collector { | ... | @@ -346,7 +346,7 @@ impl Collector { |
| 346 | self.system.refresh_processes_specifics( | 346 | self.system.refresh_processes_specifics( |
| 347 | ProcessesToUpdate::All, | 347 | ProcessesToUpdate::All, |
| 348 | true, | 348 | true, |
| 349 | process_refresh_kind(), | 349 | process_discovery_refresh_kind(), |
| 350 | ); | 350 | ); |
| 351 | self.cpu_baseline_refreshed_at = Some(Instant::now()); | 351 | self.cpu_baseline_refreshed_at = Some(Instant::now()); |
| 352 | } | 352 | } |
| ... | @@ -361,7 +361,7 @@ impl Collector { | ... | @@ -361,7 +361,7 @@ impl Collector { |
| 361 | self.system.refresh_processes_specifics( | 361 | self.system.refresh_processes_specifics( |
| 362 | ProcessesToUpdate::All, | 362 | ProcessesToUpdate::All, |
| 363 | true, | 363 | true, |
| 364 | process_refresh_kind(), | 364 | process_discovery_refresh_kind(), |
| 365 | ); | 365 | ); |
| 366 | self.cpu_baseline_refreshed_at = Some(Instant::now()); | 366 | self.cpu_baseline_refreshed_at = Some(Instant::now()); |
| 367 | | 367 | |
| ... | @@ -397,15 +397,57 @@ impl Collector { | ... | @@ -397,15 +397,57 @@ impl Collector { |
| 397 | roots.insert(config.root_pid); | 397 | roots.insert(config.root_pid); |
| 398 | let tracked = select_tracked_pids(&rows, &roots); | 398 | let tracked = select_tracked_pids(&rows, &roots); |
| 399 | let tracked_process_count = tracked.len(); | 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 | let mut processes = tracked | 434 | let mut processes = tracked |
| 401 | .into_iter() | 435 | .into_iter() |
| 402 | .filter_map(|pid| { | 436 | .filter_map(|pid| { |
| 403 | let process = self.system.process(Pid::from_u32(pid))?; | 437 | let process = self.system.process(Pid::from_u32(pid))?; |
| 404 | let disk_usage = process.disk_usage(); | 438 | let details = sample_details.process(Pid::from_u32(pid))?; |
| 405 | let command = if process.cmd().is_empty() { | 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 | process.name().to_string_lossy().into_owned() | 448 | process.name().to_string_lossy().into_owned() |
| 407 | } else { | 449 | } else { |
| 408 | process | 450 | details |
| 409 | .cmd() | 451 | .cmd() |
| 410 | .iter() | 452 | .iter() |
| 411 | .map(|part| part.to_string_lossy()) | 453 | .map(|part| part.to_string_lossy()) |
| ... | @@ -429,14 +471,15 @@ impl Collector { | ... | @@ -429,14 +471,15 @@ impl Collector { |
| 429 | ), | 471 | ), |
| 430 | cpu_percent: process.cpu_usage(), | 472 | cpu_percent: process.cpu_usage(), |
| 431 | cpu_time_ms: process.accumulated_cpu_time(), | 473 | cpu_time_ms: process.accumulated_cpu_time(), |
| 432 | resident_bytes: process.memory(), | 474 | resident_bytes: details.memory(), |
| 433 | virtual_bytes: process.virtual_memory(), | 475 | virtual_bytes: details.virtual_memory(), |
| 434 | io_read_bytes: disk_usage.total_read_bytes, | 476 | io_read_bytes: disk_usage.total_read_bytes, |
| 435 | io_write_bytes: disk_usage.total_written_bytes, | 477 | io_write_bytes: disk_usage.total_written_bytes, |
| 436 | io_semantics: io_semantics(), | 478 | io_semantics: io_semantics(), |
| 437 | }) | 479 | }) |
| 438 | }) | 480 | }) |
| 439 | .collect::<Vec<_>>(); | 481 | .collect::<Vec<_>>(); |
| | 482 | drop(process_details); |
| 440 | processes.sort_by_key(|process| process.pid); | 483 | processes.sort_by_key(|process| process.pid); |
| 441 | self.sequence = self.sequence.saturating_add(1); | 484 | self.sequence = self.sequence.saturating_add(1); |
| 442 | | 485 | |
| ... | @@ -459,6 +502,15 @@ impl Collector { | ... | @@ -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 | fn process_refresh_kind() -> ProcessRefreshKind { | 514 | fn process_refresh_kind() -> ProcessRefreshKind { |
| 463 | ProcessRefreshKind::nothing() | 515 | ProcessRefreshKind::nothing() |
| 464 | .with_memory() | 516 | .with_memory() |
| ... | @@ -472,6 +524,10 @@ fn inaccessible_process_count(selected: usize, materialized: usize) -> usize { | ... | @@ -472,6 +524,10 @@ fn inaccessible_process_count(selected: usize, materialized: usize) -> usize { |
| 472 | selected.saturating_sub(materialized) | 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 | fn remaining_cpu_measurement_delay( | 531 | fn remaining_cpu_measurement_delay( |
| 476 | baseline_refreshed_at: Option<Instant>, | 532 | baseline_refreshed_at: Option<Instant>, |
| 477 | now: Instant, | 533 | now: Instant, |
| ... | @@ -889,6 +945,39 @@ mod tests { | ... | @@ -889,6 +945,39 @@ mod tests { |
| 889 | assert!(!matches_external_identity(10_000, Some(9_999))); | 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 | #[test] | 981 | #[test] |
| 893 | fn decodes_protocol_commands() { | 982 | fn decodes_protocol_commands() { |
| 894 | let configure = serde_json::from_str::<Command>( | 983 | let configure = serde_json::from_str::<Command>( |