diff --git a/src/sensors.rs b/src/sensors.rs
index 73e67b8..1b89878 100644
--- a/src/sensors.rs
+++ b/src/sensors.rs
@@ -122,6 +122,16 @@ pub fn cpu_frequencies() -> Vec<(usize, u64)> {
out
}
+/// Max frequency (kHz) advertised for a logical cpu (its turbo ceiling).
+pub fn cpu_max_freq(cpu: usize) -> Option<u64> {
+ read_int(
+ &Path::new("/sys/devices/system/cpu")
+ .join(format!("cpu{cpu}"))
+ .join("cpufreq/cpuinfo_max_freq"),
+ )
+ .map(|v| v as u64)
+}
+
/// A RAPL power domain with an `energy_uj` counter, e.g. package or core.
pub struct RaplDomain {
pub id: String, // sysfs dir name, e.g. "intel-rapl:0"
diff --git a/src/ui.rs b/src/ui.rs
index 4891f2d..0e47b15 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -35,12 +35,21 @@ pub struct App {
peak_ring: Vec<(Instant, f64)>,
package_watts: Option<f64>,
package_temp: Option<f64>,
- p_temps: Vec<(String, f64)>,
- e_temps: Vec<(String, f64)>,
- freq_max_khz: u64,
+ /// Per physical core: label (coretemp "Core N"), freq kHz, bar-max kHz
+ /// (that core's cpuinfo_max_freq), temperature °C.
+ cores: Vec<CoreRow>,
last_pkg_energy: Option<(u64, Instant)>,
}
+/// One physical core's row in the per-core section.
+struct CoreRow {
+ label: String,
+ e_core: bool,
+ freq_khz: Option<u64>,
+ freq_max_khz: u64,
+ temp: Option<f64>,
+}
+
impl App {
pub fn new() -> App {
App {
@@ -49,9 +58,7 @@ impl App {
peak_ring: Vec::new(),
package_watts: None,
package_temp: None,
- p_temps: Vec::new(),
- e_temps: Vec::new(),
- freq_max_khz: 0,
+ cores: Vec::new(),
last_pkg_energy: None,
}
}
@@ -80,24 +87,47 @@ impl App {
.iter()
.find(|(l, _)| l.contains("Package"))
.map(|(_, v)| *v);
- self.p_temps = temps
- .iter()
- .filter(|(l, _)| !l.contains("Package") && !is_e_core_label(l))
- .cloned()
- .collect();
- self.e_temps = temps
+
+ // Per physical core: coretemp label "Core N" N == topology
+ // core_id; cpufreq is per logical cpu, so a core's speed is the
+ // max across its HT siblings. Verified mapping for the 12600KF:
+ // P core_ids 0/4/8/12/16/20 → cpus (0,1)..(10,11), E 28..31 →
+ // cpu12..15 (no HT).
+ let freqs = sensors::cpu_frequencies();
+ self.cores = temps
.iter()
- .filter(|(l, _)| is_e_core_label(l))
- .cloned()
+ .filter(|(l, _)| l.starts_with("Core "))
+ .map(|(label, t)| {
+ let core_id: u64 = label
+ .strip_prefix("Core ")
+ .and_then(|d| d.parse().ok())
+ .unwrap_or(u64::MAX);
+ let cpus = core_to_cpus(core_id);
+ let e_core = cpus.first().map_or(false, |c| *c >= 12);
+ let freq_khz = cpus
+ .iter()
+ .filter_map(|c| {
+ freqs.iter().find(|(cpu, _)| cpu == c).map(|(_, k)| *k)
+ })
+ .max();
+ let freq_max_khz = cpus
+ .iter()
+ .filter_map(|c| sensors::cpu_max_freq(*c))
+ .max()
+ .unwrap_or(0);
+ CoreRow {
+ label: label.clone(),
+ e_core,
+ freq_khz,
+ freq_max_khz,
+ temp: Some(*t),
+ }
+ })
.collect();
+ self.cores
+ .sort_by_key(|c| (c.e_core, c.label.clone()));
}
- self.freq_max_khz = sensors::cpu_frequencies()
- .iter()
- .map(|(_, khz)| *khz)
- .max()
- .unwrap_or(0);
-
// Watts = energy delta over the tick (poll cadence IS the sample
// interval; no extra sleep like the dump's fixed 500 ms window).
if let Some(d) = sensors::rapl_domains()
@@ -121,10 +151,20 @@ impl App {
}
}
-/// Verified coretemp mapping for the 12600KF: E-cores are Core 28..=31,
-/// P-cores are Core 0/4/8/12/16/20 (plus "Package id 0").
-fn is_e_core_label(label: &str) -> bool {
- matches!(label, "Core 28" | "Core 29" | "Core 30" | "Core 31")
+/// Verified topology mapping for the 12600KF: physical core N with HT has
+/// logical cpus (2k, 2k+1); P core_ids 0/4/8/12/16/20 map to pairs starting
+/// at core_id/2·2. E-cores 28..31 map 1:1 to cpu12..15.
+fn core_to_cpus(core_id: u64) -> Vec<usize> {
+ match core_id {
+ 0 => vec![0, 1],
+ 4 => vec![2, 3],
+ 8 => vec![4, 5],
+ 12 => vec![6, 7],
+ 16 => vec![8, 9],
+ 20 => vec![10, 11],
+ 28..=31 => vec![core_id as usize - 16],
+ _ => Vec::new(),
+ }
}
fn temp_color(t: f64) -> Color {
@@ -266,46 +306,47 @@ pub fn draw(f: &mut Frame, app: &App) {
));
lines.push(Line::from(""));
- // ---- P cores ----
- lines.push(Line::from(Span::styled(
- "P-cores",
- Style::default().fg(Color::Cyan),
- )));
- for (label, t) in &app.p_temps {
- lines.push(meter_line(
- label,
- &format!("{:.0} °C", t),
- Some(t / TEMP_BAR_MAX),
- temp_color(*t),
- w,
- None,
- ));
- }
- lines.push(Line::from(""));
-
- // ---- E cores ----
- lines.push(Line::from(Span::styled(
- "E-cores",
- Style::default().fg(Color::Cyan),
- )));
- for (label, t) in &app.e_temps {
- lines.push(meter_line(
- label,
- &format!("{:.0} °C", t),
- Some(t / TEMP_BAR_MAX),
- temp_color(*t),
- w,
- None,
- ));
+ // ---- cores: freq bar + temp number ----
+ for (section, pred) in [("P-cores", false), ("E-cores", true)] {
+ lines.push(Line::from(Span::styled(
+ section,
+ Style::default().fg(Color::Cyan),
+ )));
+ for core in app.cores.iter().filter(|c| c.e_core == pred) {
+ let text = match (core.freq_khz, core.temp) {
+ (Some(f), Some(t)) => format!(
+ "{:.2}GHz {:.0}°C",
+ f as f64 / 1_000_000.0,
+ t
+ ),
+ (Some(f), None) => format!("{:.2}GHz", f as f64 / 1_000_000.0),
+ (None, Some(t)) => format!("{:.0}°C", t),
+ (None, None) => "n/a".to_string(),
+ };
+ let ratio = match (core.freq_khz, core.freq_max_khz) {
+ (Some(f), max) if max > 0 => Some(f as f64 / max as f64),
+ _ => None,
+ };
+ let color = match core.temp {
+ Some(t) => temp_color(t),
+ None => Color::DarkGray,
+ };
+ lines.push(meter_line(&core.label, &text, ratio, color, w, None));
+ }
+ lines.push(Line::from(""));
}
- lines.push(Line::from(""));
// ---- footer ----
lines.push(Line::from(vec![
Span::styled("Max freq: ", Style::default().fg(Color::Cyan)),
Span::raw(format!(
"{:.2} GHz",
- app.freq_max_khz as f64 / 1_000_000.0
+ app.cores
+ .iter()
+ .filter_map(|c| c.freq_khz)
+ .max()
+ .unwrap_or(0) as f64
+ / 1_000_000.0
)),
Span::styled(" q: quit", Style::default().fg(Color::DarkGray)),
]));
TUI: per-core freq bars (HT-pair max) + temp number, per-core turbo ceiling scale
a4b120f828b8d548e3acbdc550df7dba61ef1184
josie <administrator@josie-c.com> · 2026-08-29T12:30 ·
browse files at this commit
parents:
3b726d9