authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-04 13:06:10-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 13:06:10-07:00
log163d86a78430d23414abde323ca70d8379280c0b
tree5e275b7a6f6f3556efa4f757863dfe720bc180c4
parent8e056a0e5124775b9439751667bac483d7b5320a
signature Signed by PGP key B5690EEEBB952194

perf(server): skip unused Linux process detail reads (#9768)


1 files changed, 96 insertions(+), 7 deletions(-)

native/resource-monitor/src/main.rs+96-7
...@@ -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());
367367
...@@ -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 = tracked434 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 process450 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);
442485
...@@ -459,6 +502,15 @@ impl Collector {...@@ -459,6 +502,15 @@ impl Collector {
459 }502 }
460}503}
461504
505// Keep CPU baselines separate. Even a metadata refresh resets Linux process times.
506fn 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
462fn process_refresh_kind() -> ProcessRefreshKind {514fn 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}
474526
527fn matches_process_start_time(discovered: u64, detail: u64, epoch_offset: i128) -> bool {
528 i128::from(detail) - epoch_offset == i128::from(discovered)
529}
530
475fn remaining_cpu_measurement_delay(531fn 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 }
891947
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>(