perf: Linux per-thread CPU table; main_in and faults print without it

This commit is contained in:
chairmanpow
2026-10-04 15:59:43 +01:00
parent ca662f816f
commit 8613d99629
2 changed files with 66 additions and 32 deletions
@@ -424,8 +424,38 @@ fn drive_live_fps(
})
.collect::<Vec<_>>()
.join(",");
// Per thread, ms per frame; threads sharing a name (a pool) are summed.
let threads = match (&probe.threads_at_start, crate::perf::thread_cpu_table()) {
// The main-thread split and the page-fault count are Linux-capable; only the
// per-thread table is macOS, so they print without it.
let inside = main_split
.inside_ms()
.map(|m| format!(" main_in={m:.2}"))
.unwrap_or_default();
let now_still = still_inputs_now();
let noisy: Vec<u32> = now_still
.iter()
.zip(probe.still_at_start)
.map(|(n, s)| n.wrapping_sub(s))
.collect();
// Page faults per frame: `sys` time without syscalls is the kernel
// zero-filling pages.
let faults = match (probe.faults_at_start, crate::perf::process_faults()) {
(Some((mi0, ma0)), Some((mi1, ma1))) => format!(
" faults={:.0}/{:.1} noisy=[cam:{},dbg:{},view:{},win:{},claim:{},portals:{},flips:{}]",
(mi1 - mi0) as f64 / v_len as f64,
(ma1 - ma0) as f64 / v_len as f64,
noisy[0],
noisy[1],
noisy[2],
noisy[3],
noisy[4],
noisy[5],
noisy[6],
),
_ => String::new(),
};
// Per thread, ms per frame; threads sharing a name (a pool) are summed. The
// table is macOS/Linux; elsewhere it is absent and `main_in`/`faults` stand alone.
let mut threads = match (&probe.threads_at_start, crate::perf::thread_cpu_table()) {
(Some(t0), Some(t1)) => {
let start: std::collections::HashMap<usize, (f64, f64)> =
t0.iter().map(|(k, _, u, s)| (*k, (*u, *s))).collect();
@@ -451,38 +481,12 @@ fn drive_live_fps(
.map(|(n, (u, s))| format!("{n}:{:.2}+{:.2}", per(*u), per(*s)))
.collect::<Vec<_>>()
.join(",");
let inside = main_split
.inside_ms()
.map(|m| format!(" main_in={m:.2}"))
.unwrap_or_default();
let now_still = still_inputs_now();
let noisy: Vec<u32> = now_still
.iter()
.zip(probe.still_at_start)
.map(|(n, s)| n.wrapping_sub(s))
.collect();
// Page faults per frame: `sys` time without syscalls is the kernel
// zero-filling pages.
let faults =
match (probe.faults_at_start, crate::perf::process_faults()) {
(Some((mi0, ma0)), Some((mi1, ma1))) => format!(
" faults={:.0}/{:.1} noisy=[cam:{},dbg:{},view:{},win:{},claim:{},portals:{},flips:{}]",
(mi1 - mi0) as f64 / v_len as f64,
(ma1 - ma0) as f64 / v_len as f64,
noisy[0],
noisy[1],
noisy[2],
noisy[3],
noisy[4],
noisy[5],
noisy[6],
),
_ => String::new(),
};
format!(" thr=[{list}]{inside}{faults}")
format!(" thr=[{list}]")
}
_ => String::new(),
};
threads.push_str(&inside);
threads.push_str(&faults);
format!(
" cpu_ms={per_frame_ms:.2} cpu_pct={:.0} cpu_p95={:.2} cpu_p99={:.2} \
frames_over={over} dropped={dropped} paced_max={paced_max:.1} worst=[{worst}]{threads}",
+31 -1
View File
@@ -244,7 +244,37 @@ pub(crate) fn thread_cpu_table() -> Option<Vec<(usize, String, f64, f64)>> {
}
Some(out)
}
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
{
// `/proc/self/task/<tid>/stat`: one line per thread, the comm in parens then the fields
// from field 3 (state), so `utime` (field 14) and `stime` (field 15) sit at rest-index 11
// and 12. The comm may contain spaces and parens, so split at the LAST `)`. A thread that
// dies between the readdir and its stat read is skipped.
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
if ticks <= 0.0 {
return None;
}
let mut out = Vec::new();
for entry in std::fs::read_dir("/proc/self/task").ok()? {
let Ok(entry) = entry else { continue };
let tid: usize = entry.file_name().to_string_lossy().parse().ok()?;
let Ok(stat) = std::fs::read_to_string(entry.path().join("stat")) else {
continue;
};
let Some(close) = stat.rfind(')') else { continue };
let name = stat[..close].trim_start_matches('(').to_string();
let fields: Vec<&str> = stat[close + 1..].split_whitespace().collect();
let (Some(u), Some(s)) = (fields.get(11), fields.get(12)) else {
continue;
};
let (Ok(u), Ok(s)) = (u.parse::<f64>(), s.parse::<f64>()) else {
continue;
};
out.push((tid, name, u / ticks, s / ticks));
}
Some(out)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
None
}