perf: Linux per-thread CPU table; main_in and faults print without it
This commit is contained in:
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user