powermetrics/GPUwall.sh

222 lines
8.9 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 ($af, $active, $idle, $pwr, $cpu_power_mw, $thermal_state);
my %bins;
my (@cluster_order, %clusters, %cluster_power);
sub pct_color {
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, $fg) = @_;
$pct = 0 if !defined $pct || $pct < 0;
$pct = 100 if $pct > 100;
my $n = int($pct * $w / 100 + 0.5);
return sprintf("\e[38;5;%sm%s\e[0m%s", $fg, ("█" x $n), ("░" x ($w - $n)));
}
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 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/, $_) {
$af = $1 if $line =~ /^GPU HW active frequency:\s*([\d.]+)\s*MHz/;
if ($line =~ /^GPU(?: HW)? active residency:\s*([\d.]+)%\s*\((.*)\)/) {
$active = $1;
my @pairs = $2 =~ /([\d.]+)\s*MHz:\s*([\d.]+)%/g;
for (my $i = 0; $i < @pairs; $i += 2) {
$bins{$pairs[$i]} += $pairs[$i + 1];
}
}
$idle = $1 if $line =~ /^GPU idle residency:\s*([\d.]+)%/;
$pwr = $1 if $line =~ /^GPU Power:\s*([\d.]+)\s*mW/;
$cpu_power_mw = $1 if $line =~ /^CPU Power:\s*([\d.]+)\s*mW/;
$thermal_state = $1 if $line =~ /pressure.*:\s*([A-Za-z]+)/i;
if ($line =~ /^((?:E-Cluster|P\d+-Cluster|P-Cluster) HW active residency):\s*([\d.]+)%\s*(?:\((.*)\))?/) {
my ($name, $active_res, $hist) = ($1, $2, $3 // "");
push @cluster_order, $name unless exists $clusters{$name};
my ($max_res, $weighted_res) = parse_hist($hist);
my $eff = $max_res ? $active_res * $weighted_res / $max_res : $active_res;
(my $short = $name) =~ s/\s+HW active residency$//;
$clusters{$name} = { eff => $eff, short => $short };
}
elsif ($line =~ /^((?:E-Cluster|P\d+-Cluster|P-Cluster)) Power:\s*([\d.]+)\s*mW/) {
$cluster_power{$1} = $2 / 1000;
}
}
my $max = 0;
for my $f (keys %bins) {
$max = $f if $f > $max;
}
my $used = ($max && defined $af && defined $active) ? ($active * $af / $max) : ($active // 0);
my $freq_pct = ($max && defined $af) ? (100 * $af / $max) : 0;
my $cols = $ENV{COLUMNS} || 120;
my $bigw = $cols > 170 ? 54 : $cols > 135 ? 42 : 30;
my $smallw = $cols > 170 ? 36 : $cols > 135 ? 28 : 20;
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;97mGPU WALL\e[0m sample ${sample_s}s hold ${hold_s}s\n";
print "\e[38;5;240m$rule\e[0m\n\n";
printf " used_of_max %6.2f%% %s\n", $used, bar($used, $bigw, pct_color($used));
printf " active %6.2f%% %s\n", $active, bar($active, $bigw, pct_color($active));
printf " freq_of_max %6.2f%% %s\n", $freq_pct, bar($freq_pct, $bigw, 39);
printf " idle %6.2f%% %s\n\n", $idle, bar($idle, $bigw, 244);
printf " active freq %6.0f MHz\n", $af if defined $af;
printf " top bin %6.0f MHz\n", $max if $max;
printf " power est %6.2f W\n", ($pwr / 1000) if defined $pwr;
print "\n\e[1;97m frequency mix\e[0m\n\n";
for my $f (sort { $a <=> $b } keys %bins) {
next if $bins{$f} <= 0;
printf " %4d MHz %6.2f%% %s\n",
$f, $bins{$f}, bar($bins{$f}, $smallw, 45);
}
my ($cpu_hot_w, $cpu_hot_name) = (undef, "CPU hotspot est");
my ($cpu_eff_total, $cpu_eff_n) = (0, 0);
for my $cluster (@cluster_order) {
$cpu_eff_total += $clusters{$cluster}{eff};
$cpu_eff_n++;
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;
}
}
$cpu_eff_total = $cpu_eff_n ? ($cpu_eff_total / $cpu_eff_n) : 0;
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 $pwr ? ($pwr / 1000) : undef;
my $display_component_w = defined $used && $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 $used ? ((0.04 * $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 "\n\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