powermetrics/CPUwall.sh

242 lines
9.5 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
sample_ms="${SAMPLE_MS:-5000}"
hold="${HOLD_SEC:-6}"
if [[ "${EUID}" -ne 0 ]]; then
exec sudo --preserve-env=SAMPLE_MS,HOLD_SEC bash "$0" "$@"
fi
cleanup() {
printf '\033[0m\033[?25h\n'
}
trap cleanup INT TERM EXIT
printf '\033[?25l'
while :; do
out="$(
powermetrics --samplers cpu_power,gpu_power,thermal -i "${sample_ms}" -n 1 |
SAMPLE_MS="${sample_ms}" HOLD_SEC="${hold}" COLUMNS="${COLUMNS:-120}" perl -0ne '
use strict;
use warnings;
use utf8;
binmode STDOUT, ":utf8";
my (@cluster_order, %clusters, %cores, %cluster_power);
my $current = "UNGROUPED";
my ($cpu_power_mw, $gpu_power_mw, $gpu_af, $gpu_active, $gpu_idle, $thermal_state);
my %gpu_bins;
sub parse_hist {
my ($hist) = @_;
my @pairs = ($hist // "") =~ /([\d.]+)\s*MHz:\s*([\d.]+)%/g;
my ($max, $weighted) = (0, 0);
for (my $i = 0; $i < @pairs; $i += 2) {
$max = $pairs[$i] if $pairs[$i] > $max;
$weighted += $pairs[$i] * $pairs[$i + 1] / 100;
}
return ($max, $weighted);
}
sub color_pct {
my ($p) = @_;
return 46 if $p < 25;
return 118 if $p < 50;
return 226 if $p < 75;
return 208 if $p < 90;
return 196;
}
sub bar {
my ($pct, $w) = @_;
$pct = 0 if !defined $pct || $pct < 0;
$pct = 100 if $pct > 100;
my $n = int($pct * $w / 100 + 0.5);
return ("█" x $n) . ("░" x ($w - $n));
}
sub pressure_factor {
my ($state) = @_;
return 1.00 if !defined $state;
return 1.00 if $state =~ /nominal/i;
return 1.08 if $state =~ /fair|moderate/i;
return 1.18 if $state =~ /serious|heavy/i;
return 1.30 if $state =~ /critical|trapping/i;
return 1.05;
}
sub state_color {
my ($state) = @_;
return 46 if !defined $state || $state =~ /nominal/i;
return 226 if $state =~ /fair|moderate/i;
return 208 if $state =~ /serious|heavy/i;
return 196 if $state =~ /critical|trapping/i;
return 250;
}
for my $line (split /\n/, $_) {
if ($line =~ /^((?:E-Cluster|P\d+-Cluster|P-Cluster) HW active residency):\s*([\d.]+)%\s*(?:\((.*)\))?/) {
my ($name, $active, $hist) = ($1, $2, $3 // "");
$current = $name;
push @cluster_order, $name unless exists $clusters{$name};
my ($max, $weighted) = parse_hist($hist);
my $eff = $max ? $active * $weighted / $max : $active;
(my $short = $name) =~ s/\s+HW active residency$//;
$clusters{$name} = { active => $active, eff => $eff, max => $max, short => $short };
}
elsif ($line =~ /^(CPU \d+) active residency:\s*([\d.]+)%\s*(?:\((.*)\))?/) {
my ($cpu, $active, $hist) = ($1, $2, $3 // "");
my ($max, $weighted) = parse_hist($hist);
my $eff = $max ? $active * $weighted / $max : $active;
push @{ $cores{$current} }, { cpu => $cpu, active => $active, eff => $eff, max => $max };
}
elsif ($line =~ /^((?:E-Cluster|P\d+-Cluster|P-Cluster)) Power:\s*([\d.]+)\s*mW/) {
$cluster_power{$1} = $2 / 1000;
}
elsif ($line =~ /^CPU Power:\s*([\d.]+)\s*mW/) {
$cpu_power_mw = $1;
}
elsif ($line =~ /^GPU HW active frequency:\s*([\d.]+)\s*MHz/) {
$gpu_af = $1;
}
elsif ($line =~ /^GPU(?: HW)? active residency:\s*([\d.]+)%\s*\((.*)\)/) {
$gpu_active = $1;
my @pairs = $2 =~ /([\d.]+)\s*MHz:\s*([\d.]+)%/g;
for (my $i = 0; $i < @pairs; $i += 2) {
$gpu_bins{$pairs[$i]} += $pairs[$i + 1];
}
}
elsif ($line =~ /^GPU idle residency:\s*([\d.]+)%/) {
$gpu_idle = $1;
}
elsif ($line =~ /^GPU Power:\s*([\d.]+)\s*mW/) {
$gpu_power_mw = $1;
}
elsif ($line =~ /pressure.*:\s*([A-Za-z]+)/i) {
$thermal_state = $1;
}
}
my $cols = $ENV{COLUMNS} || 120;
my $barw = $cols > 170 ? 44 : $cols > 135 ? 34 : 24;
my $rule = "─" x ($cols > 100 ? 92 : 70);
my $sample_s = (($ENV{SAMPLE_MS} || 5000) / 1000);
my $hold_s = $ENV{HOLD_SEC} || 6;
my $display_bias_w = defined $ENV{DISPLAY_BIAS_W} ? $ENV{DISPLAY_BIAS_W} : 2.5;
my $clamshell_factor = defined $ENV{CLAMSHELL_FACTOR} ? $ENV{CLAMSHELL_FACTOR} : 1.08;
my $ambient_c = defined $ENV{AMBIENT_C} ? $ENV{AMBIENT_C} : 23;
my $idle_offset_c = defined $ENV{IDLE_OFFSET_C} ? $ENV{IDLE_OFFSET_C} : 9;
my $cpu_theta = defined $ENV{CPU_THETA_C_PER_W} ? $ENV{CPU_THETA_C_PER_W} : 1.55;
my $gpu_theta = defined $ENV{GPU_THETA_C_PER_W} ? $ENV{GPU_THETA_C_PER_W} : 1.35;
print "\e[1;97mCPU WALL\e[0m sample ${sample_s}s hold ${hold_s}s\n";
print "\e[38;5;240m$rule\e[0m\n\n";
for my $cluster (@cluster_order) {
my $c = $clusters{$cluster};
my $cluster_color = $cluster =~ /^E-/ ? 45 : 213;
my $cc = color_pct($c->{eff});
printf "\e[1;38;5;%sm%-28s\e[0m \e[38;5;%sm%s\e[0m %6.2f%% a:%6.2f%%",
$cluster_color, $cluster, $cc, bar($c->{eff}, $barw), $c->{eff}, $c->{active};
printf " max:%4d MHz", $c->{max} if $c->{max};
print "\n";
for my $r (@{ $cores{$cluster} || [] }) {
my $pc = color_pct($r->{eff});
printf " %-6s \e[38;5;%sm%s\e[0m %6.2f%% a:%6.2f%%",
$r->{cpu}, $pc, bar($r->{eff}, $barw), $r->{eff}, $r->{active};
printf " max:%4d MHz", $r->{max} if $r->{max};
print "\n";
}
print "\n";
}
my ($gpu_max, $cpu_eff_total, $cpu_eff_n) = (0, 0, 0);
for my $cluster (@cluster_order) {
$cpu_eff_total += $clusters{$cluster}{eff};
$cpu_eff_n++;
}
$cpu_eff_total = $cpu_eff_n ? ($cpu_eff_total / $cpu_eff_n) : 0;
for my $f (keys %gpu_bins) {
$gpu_max = $f if $f > $gpu_max;
}
my $gpu_used = ($gpu_max && defined $gpu_af && defined $gpu_active)
? ($gpu_active * $gpu_af / $gpu_max)
: (defined $gpu_active ? $gpu_active : undef);
my $cpu_hot_w;
my $cpu_hot_name = "CPU hotspot est";
for my $cluster (@cluster_order) {
my $short = $clusters{$cluster}{short};
next unless exists $cluster_power{$short};
if (!defined $cpu_hot_w || $cluster_power{$short} > $cpu_hot_w) {
$cpu_hot_w = $cluster_power{$short};
$cpu_hot_name = $short;
}
}
my $cpu_power_w = defined $cpu_power_mw ? ($cpu_power_mw / 1000) : undef;
if (!defined $cpu_hot_w && defined $cpu_power_w) {
my $hot_frac = $cpu_eff_total >= 85 ? 0.62
: $cpu_eff_total >= 60 ? 0.52
: $cpu_eff_total >= 35 ? 0.42
: 0.32;
$cpu_hot_w = $cpu_power_w * $hot_frac;
}
my $gpu_power_w = defined $gpu_power_mw ? ($gpu_power_mw / 1000) : undef;
my $display_component_w = defined $gpu_used && $gpu_used >= 20 ? ($display_bias_w * 0.70) : $display_bias_w;
my $gpu_zone_w = defined $gpu_power_w
? ($gpu_power_w + $display_component_w)
: (defined $gpu_used ? ((0.04 * $gpu_used) + $display_component_w) : undef);
my $thermal_mult = pressure_factor($thermal_state) * $clamshell_factor;
my $cpu_abs_c = defined $cpu_hot_w
? ($ambient_c + $idle_offset_c + ($cpu_hot_w * $cpu_theta * $thermal_mult))
: undef;
my $gpu_abs_c = defined $gpu_zone_w
? ($ambient_c + $idle_offset_c + ($gpu_zone_w * $gpu_theta * $thermal_mult))
: undef;
my $gradient_c = (defined $cpu_abs_c && defined $gpu_abs_c)
? abs($cpu_abs_c - $gpu_abs_c)
: undef;
my $gradient_side = !defined $gradient_c ? "insufficient data"
: $cpu_abs_c >= $gpu_abs_c ? "CPU side hotter"
: "GPU/display side hotter";
my $basis = (defined $cpu_power_w && defined $gpu_power_w) ? "power-based estimate" : "util fallback";
my $state = defined $thermal_state ? $thermal_state : "n/a";
my $state_color = state_color($thermal_state);
print "\e[1;97mTHERMAL ESTIMATE\e[0m\n";
print "\e[38;5;240m$rule\e[0m\n\n";
printf " pressure \e[38;5;%sm%s\e[0m\n", $state_color, $state;
printf " ambient est %6.2f C\n", $ambient_c;
printf " cpu hotspot %6.2f W %s\n", $cpu_hot_w, $cpu_hot_name if defined $cpu_hot_w;
if (defined $gpu_zone_w) {
if (defined $gpu_power_w) {
printf " gpu zone %6.2f W gpu %.2f + display %.2f\n", $gpu_zone_w, $gpu_power_w, $display_component_w;
} else {
printf " gpu zone %6.2f W util fallback + display %.2f\n", $gpu_zone_w, $display_component_w;
}
}
printf " cpu abs est %6.2f C\n", $cpu_abs_c if defined $cpu_abs_c;
printf " gpu abs est %6.2f C\n", $gpu_abs_c if defined $gpu_abs_c;
printf " est gradient %6.2f C %s\n", $gradient_c, $gradient_side if defined $gradient_c;
printf " basis %s\n", $basis;
printf " model idle %.1fC + zone_w * theta * pressure * clamshell\n", $idle_offset_c;
printf " theta cpu %.2fC/W gpu %.2fC/W\n", $cpu_theta, $gpu_theta;
print " note estimated absolute temps, not sensor readings\n";
'
)"
printf '\033[2J\033[H'
printf '\033[1;37m%s\033[0m\n\n' "$(date '+%Y-%m-%d %H:%M:%S')"
printf '%s\n' "${out}"
sleep "${hold}"
done