Files
MobileGL/tools/device_bench/pin_device.sh
T
swung0x48 06b61bd84e [MG_Util, MG_Remote, tools] (P6): the wire counters and the spawn server's stats latch
Gate 8's last two numbers (SEG_STAGE bytes/frame, records/frame post-
chunking) had no producer at all: the summary line's byte classes are
backend-to-driver bytes and maxrec is a max, not a per-frame. Two push-
guarded counters answer them at their single choke points - ByteClass::
StageSegmentBytes at PipeWireCodec's StageAllocate (the only allocation
behind StageBytes, so every buffer walk, texture slab and CSO archive
lands there and emitters cannot double-count) and CallClass::WireRecords
on EncodeRecord's committed path (post-chunking an emitter no longer
knows how many records its one call produced). A staged-blob histogram
rides along. G1 holds: symbols 0/0/0/0, .text byte-identical, pull build
carries no MG_Remote symbols.

The spawn server never counted anything: PipeStats::Init() is a step of
MobileGL::Initialize(), which ServerMain never reaches, so every Enabled()
site in that process - including ServerLoop's own ServerWaits/ServerParks
publish - stayed false and the server log carried no summary line at all
(a zero that means 'another process' printed in the shape of one that
means 'never waited' is the milder misreading; the line was simply
absent). ServerMain now arms and closes PipeStats at its role boundary -
after MG_ConfigLoader::Init (Init latches from the parsed config), after
the server-role latch (the log sink's role), and before ::_exit(0).
Red-once: 0 server summary lines before, 29 after, SSIM 1.0 unchanged.

MOBILEGL_PIPE_STATS_FILE now derives per role through the log sink's
exported RoleLogPath rule - run.client.json / run.server.json, the bare
name resolves to no file - because both roles reach FormatJson() and ofstream
trunc made the second writer replace the first. G1 caught three separate
leaks on the way (a string temporary in the monolith body, one resized
Shutdown() from unequal #if arms, two rewritten MGLOG_W literals in
.rodata); the pull arm is statement-for-statement the original.

Device tooling for the paired A/B lands with it: pin_device.sh gains a
measured 2f7cbe2e entry (this board clamps the GPU at 1050 MHz -
thermal_pwrlevel writes 0 and reads back 1, max_gpuclk is read-only;
absolute times are not comparable with the 1100 MHz-era rows), and
tools/device_bench/p6/ carries the session collector and renderer beside
run_android_retrace_local.py's MOBILEGL_TRACE_PACKAGE override.

Unit 2367/2367 (PipeStats 25/25), spawn integration 102/102.
2026-09-22 03:37:51 -04:00

394 lines
20 KiB
Bash
Executable File

#!/usr/bin/env bash
# pin_device.sh <serial> pin|unpin|check
#
# Frequency pinning for the two disaggregation-campaign devices, standalone: pure adb + su, no
# sourcing of a device profile, no dependency on bench.sh. It exists because bench.sh's
# pin_freqs()/unpin_freqs() are MediaTek-legacy-only (/proc/ppm + /proc/gpufreq) and NEITHER of
# these two devices has those paths - on both of them bench.sh's pin writes into nothing and
# exits 0, which is precisely the silent "the run looks pinned but is not" failure the
# PROFILE_VERIFIED guard was built to catch. So: pin here, run bench.sh with --no-pin, and use
# `check` in place of the pin-integrity fields bench.sh would otherwise have sampled.
#
# ./pin_device.sh 35d0befa pin
# ./bench.sh --device devices/xiaomi-adreno830.env --backend magma --no-pin
# ./pin_device.sh 35d0befa check || echo "PIN DRIFTED - discard this run"
# ./pin_device.sh 35d0befa unpin
#
# All three actions print the live big/little/GPU frequencies, the governors and the gate
# temperature, so the output of `pin` and of `unpin` is itself the before/after evidence.
#
# check exit codes (deliberately three-valued: "no pin at all" and "a pin that slipped" are
# different facts, and collapsing them to one non-zero would hide which one happened):
# 0 PINNED - every pinned node is at its pin AND every live frequency equals it
# 1 DRIFT - the device is partly pinned, or a live frequency has left its pin. The
# dangerous state: a run overlapping this is not comparable. Discard it.
# 2 UNPINNED- no node is at a pin this script set; the vendor governors have their range
# back. That is the correct state to leave a device in, and it is still non-zero
# so that `pin_device.sh X check && measure` cannot silently measure unpinned.
# Note this is asserted against OUR pins, not against the stock range: ColorOS
# moves policy4's max on its own within seconds of a release, so an exact-stock
# comparison reported DRIFT on a correctly unpinned device.
#
# Values were read off each device on 2026-09-07 and confirmed against a pinned window; see
# ../REPORT.md and the matching *.env profiles. Stock values are hardcoded rather than sampled
# at pin time on purpose - a restore that reads "stock" from an already-pinned device would
# make the pin permanent, which is how a device silently stays clamped across a reboot-less
# week of runs.
set -u -o pipefail
# Git Bash: stop MSYS rewriting /sys/... and /proc/... arguments into C:/Program Files/...
export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'
SERIAL=${1:-}
ACTION=${2:-}
case "$ACTION" in
pin|unpin|check) ;;
*) echo "usage: $0 <serial> pin|unpin|check" >&2; exit 64 ;;
esac
CPUFREQ=/sys/devices/system/cpu/cpufreq
# ---------------------------------------------------------------------------------------------
# Device table. Everything device-specific lives here; the actions below are generic.
# POLICIES - "label:policy:pinned_khz:stock_min:stock_max" per cpufreq policy to pin.
# Every policy that can run a hot thread must be listed, not just the two
# the protocol reports: on MT6993 an unpinned policy7 at 4.21 GHz defeats
# the entire pin the moment the scheduler lands a thread on it.
# THERMAL_TYPE - matched against /sys/class/thermal/thermal_zone*/type by name, never by
# zone number: numbering is not stable across boots.
# ---------------------------------------------------------------------------------------------
case "$SERIAL" in
35d0befa) # Xiaomi 24129PN74C, Snapdragon 8 Elite (SM8750), Adreno 830v2
DEV_NAME="Xiaomi 24129PN74C / SM8750 / Adreno 830v2"
GPU_STYLE=kgsl
POLICIES="big:policy6:1958400:1017600:2841600 little:policy0:1555200:556800:2745600"
THERMAL_TYPE=cpuss-0-0
KGSL=/sys/class/kgsl/kgsl-3d0
GPU_PIN_LEVEL=0 # pwrlevel 0 = 1100 MHz, above the stock devfreq ceiling of 1050
GPU_STOCK_MIN_LEVEL=12
GPU_STOCK_MAX_LEVEL=0
GPU_PINNED_FREQ=1100000000 # gpuclk is in Hz on kgsl, NOT kHz
GPU_FREQ_UNIT=Hz
;;
2f7cbe2e) # Redmi M332BF, same SoC as 35d0befa but a DIFFERENT board vendor lock
DEV_NAME="Redmi M332BF / SM8750 / Adreno 830v2"
GPU_STYLE=kgsl
# Read off THIS unit on 2026-09-22, then confirmed against a pinned window (pin written,
# every node read back equal to the pin, unpinned, every node back at the stock values
# recorded below). Same SM8750 as 35d0befa and the same policy ids, BUT THE STOCK RANGES
# DIFFER (policy6 max is 3072000 here vs 2841600 there) - which is exactly why copying the
# sibling row would have been wrong even though the two parts are the same silicon.
# Every write below needs root; the verify action re-reads all of them.
POLICIES="big:policy6:1958400:1017600:3072000 little:policy0:1555200:556800:2745600"
THERMAL_TYPE=cpuss-0-0 # thermal_zone13 on this boot; matched by type, never by number
KGSL=/sys/class/kgsl/kgsl-3d0
# THE ONE PLACE THIS DEVICE DIFFERS FROM 35d0befa'S ROW, AND IT IS MEASURED, NOT ASSUMED.
# Collapsing the kgsl pwrlevel range onto 0 does NOT reach 1100 MHz here: gpuclk reads
# 1050000000 and stays there across repeated samples, because this unit ships
# thermal_pwrlevel=1 permanently (it latches - writing 0 returns success and the node reads
# back 1) and max_gpuclk is read-only at 1050000000. devfreq's own max_freq also clamps to
# 1050000000. So the pin is "the fastest frequency this board will actually run", not "the
# top OPP in the table" - recorded as 1050 MHz so `check` compares against the truth.
# Consequence for the campaign, stated where the number is: absolute times from this device
# are NOT comparable with the 35d0befa rows taken while it could still reach 1100.
GPU_PIN_LEVEL=0 # pwrlevel 0 = the 1050 MHz ceiling this board enforces
GPU_STOCK_MIN_LEVEL=12
GPU_STOCK_MAX_LEVEL=0
GPU_PINNED_FREQ=1050000000 # gpuclk is in Hz on kgsl, NOT kHz
GPU_FREQ_UNIT=Hz
;;
3B159D009VZ00000) # Oppo PLG110 / ColorOS, MediaTek MT6993 (Dimensity 9500), Mali
DEV_NAME="Oppo PLG110 / MT6993 / Mali (gpufreqv2)"
GPU_STYLE=gpufreqv2
# 2000000 / 1600000 are the nearest available OPPs to the protocol's 1958400 / 1555200
# (+2.1% / +2.9%); neither target is an exact step on this part.
POLICIES="big:policy4:2000000:300000:3500000 little:policy0:1600000:300000:2100000 extra:policy7:2000000:300000:3200000"
THERMAL_TYPE=soc_max
GPU_FIX_NODE=/proc/gpufreqv2/fix_target_opp_index
GPU_PIN_LEVEL=0 # OPP index 0 = 1716000 kHz, the top working OPP
GPU_PINNED_FREQ=1716000
GPU_FREQ_UNIT=kHz
;;
*)
echo "$0: unknown serial '$SERIAL'." >&2
echo "Known: 35d0befa (Xiaomi/Adreno830), 2f7cbe2e (Redmi/Adreno830), 3B159D009VZ00000 (Oppo/Mali)." >&2
echo "Refusing to guess: the pin path differs per SoC and a wrong one fails silently." >&2
exit 64 ;;
esac
A="adb -s $SERIAL"
# Quote the whole su invocation for the DEVICE shell, or the redirect runs unprivileged.
su_() { $A shell "su -c '$*'" 2>&1 | tr -d '\r'; }
$A get-state >/dev/null 2>&1 || { echo "$0: device $SERIAL is not connected" >&2; exit 65; }
[ "$(su_ 'id -u')" = "0" ] || { echo "$0: no root on $SERIAL (su failed)" >&2; exit 65; }
# --- read live state in ONE device round trip -------------------------------------------------
# Every read goes through su: on the Oppo the cpufreq nodes are 0660 system:system and a plain
# `adb shell cat scaling_governor` answers "Permission denied", which a careless parser reads
# as an empty governor rather than as a failure.
read_state() {
local script="" spec label pol
for spec in $POLICIES; do
IFS=: read -r label pol _ _ _ <<<"$spec"
script="$script echo \"${label}_gov=\$(cat $CPUFREQ/$pol/scaling_governor)\";"
script="$script echo \"${label}_min=\$(cat $CPUFREQ/$pol/scaling_min_freq)\";"
script="$script echo \"${label}_max=\$(cat $CPUFREQ/$pol/scaling_max_freq)\";"
script="$script echo \"${label}_cur=\$(cat $CPUFREQ/$pol/scaling_cur_freq)\";"
done
script="$script for tz in /sys/class/thermal/thermal_zone*; do"
script="$script if [ \"\$(cat \$tz/type 2>/dev/null)\" = \"$THERMAL_TYPE\" ]; then"
script="$script echo \"temp_mc=\$(cat \$tz/temp)\"; echo \"temp_zone=\$tz\"; break; fi; done;"
# Emit the GPU nodes RAW and pick them apart locally. The device-side script is delivered as
# `su -c '<script>'`, so a single quote anywhere inside it (an awk program, a sed expression)
# closes that quoting and the whole read silently returns nothing - which `check` then reports
# as every node being "neither pin nor stock", i.e. a false DRIFT on a perfectly pinned device.
# Parsing on this side keeps the device-side text quote-free.
if [ "$GPU_STYLE" = kgsl ]; then
script="$script echo \"gpu_minlvl=\$(cat $KGSL/min_pwrlevel)\";"
script="$script echo \"gpu_maxlvl=\$(cat $KGSL/max_pwrlevel)\";"
script="$script echo \"gpu_freqraw=\$(cat $KGSL/gpuclk)\";"
script="$script echo \"gpu_busyraw=\$(cat $KGSL/gpu_busy_percentage)\";"
else
script="$script echo \"gpu_fixraw=\$(cat $GPU_FIX_NODE | head -1)\";"
script="$script echo \"gpu_freqraw=\$(cat /sys/kernel/ged/hal/current_freqency | head -1)\";"
script="$script echo \"gpu_busyraw=\$(cat /sys/kernel/ged/hal/gpu_utilization | head -1)\";"
fi
su_ "$script"
}
# gpuclk -> "222000000" ($NF)
# current_freqency -> "<opp_index> <freq_khz>" ($NF)
# gpu_busy_percentage -> "26 %" / gpu_utilization -> "7 0 100" ($1)
# fix_target_opp_index -> "[GPUFREQ-DEBUG] fix GPU/STACK OPP index: 0/0" when pinned
# -> "[GPUFREQ-DEBUG] fix GPU/STACK OPP index is disabled" when not
derive_gpu() {
ST[gpu_freq]=$(echo "${ST[gpu_freqraw]:-}" | awk '{print $NF}')
ST[gpu_busy]=$(echo "${ST[gpu_busyraw]:-}" | awk '{print $1}')
[ "$GPU_STYLE" = kgsl ] && return 0
case "${ST[gpu_fixraw]:-}" in
*"is disabled"*) ST[gpu_fix]=off ;;
*"index: "*) ST[gpu_fix]=$(echo "${ST[gpu_fixraw]}" | sed -e 's/.*index: //' -e 's|/.*||') ;;
*) ST[gpu_fix]="<unread>" ;;
esac
}
declare -A ST
load_state() {
local line k v
ST=()
while IFS= read -r line; do
k=${line%%=*}; v=${line#*=}
case "$line" in *=*) ST[$k]=$v ;; esac
done < <(read_state)
# A read that came back empty is a BROKEN READ, not an unpinned device and not a drifted one.
# Without this the classifier sees every node as "neither pin nor stock" and prints DRIFT,
# which reads as "your pin slipped" when the truth is "this script could not see the device".
# Bail loudly instead: a pin/unpin whose verification cannot run must not look like a verdict.
if [ "${#ST[@]}" -eq 0 ]; then
echo "$0: could not read any state from $SERIAL (su read returned nothing)." >&2
echo " The device-side read is delivered as su -c '<script>'; check that nothing in it" >&2
echo " contains a single quote, and that su still works: adb -s $SERIAL shell su -c id" >&2
exit 66
fi
derive_gpu
}
field() { echo "${ST[$1]:-<unread>}"; }
print_state() {
local spec label pol pin smin smax
echo " device : $DEV_NAME ($SERIAL)"
for spec in $POLICIES; do
IFS=: read -r label pol pin smin smax <<<"$spec"
printf " %-7s %-8s gov=%-10s cur=%-9s min=%-9s max=%-9s (pin %s / stock %s-%s)\n" \
"$label" "$pol" "$(field ${label}_gov)" "$(field ${label}_cur)" \
"$(field ${label}_min)" "$(field ${label}_max)" "$pin" "$smin" "$smax"
done
if [ "$GPU_STYLE" = kgsl ]; then
printf " %-7s %-8s pwrlevel=%s..%s freq=%s %s busy=%s%% (pin lvl %s = %s %s / stock lvl %s..%s)\n" \
gpu kgsl-3d0 "$(field gpu_maxlvl)" "$(field gpu_minlvl)" "$(field gpu_freq)" "$GPU_FREQ_UNIT" \
"$(field gpu_busy)" "$GPU_PIN_LEVEL" "$GPU_PINNED_FREQ" "$GPU_FREQ_UNIT" \
"$GPU_STOCK_MAX_LEVEL" "$GPU_STOCK_MIN_LEVEL"
else
printf " %-7s %-8s fix_opp=%-4s freq=%s %s busy=%s%% (pin idx %s = %s %s / stock off)\n" \
gpu gpufreqv2 "$(field gpu_fix)" "$(field gpu_freq)" "$GPU_FREQ_UNIT" "$(field gpu_busy)" \
"$GPU_PIN_LEVEL" "$GPU_PINNED_FREQ" "$GPU_FREQ_UNIT"
fi
printf " %-7s %-8s %s mC = %s C (%s)\n" thermal "$THERMAL_TYPE" \
"$(field temp_mc)" "$(awk -v t="$(field temp_mc)" 'BEGIN{if(t+0==0){print "?"}else{printf "%.1f", t/1000}}')" \
"$(field temp_zone)"
}
# --- actions ----------------------------------------------------------------------------------
# Write order is min -> floor, then max -> target, then min -> target. Setting min above the
# current max (or max below the current min) is clamped by cpufreq, so a naive two-write pin
# succeeds on one device and silently half-applies on another depending on where stock sits
# relative to the target. Dropping min to the policy floor first makes the order stock-agnostic.
do_pin() {
local spec label pol pin smin smax script=""
for spec in $POLICIES; do
IFS=: read -r label pol pin smin smax <<<"$spec"
script="$script echo \$(cat $CPUFREQ/$pol/cpuinfo_min_freq) > $CPUFREQ/$pol/scaling_min_freq;"
script="$script echo $pin > $CPUFREQ/$pol/scaling_max_freq;"
script="$script echo $pin > $CPUFREQ/$pol/scaling_min_freq;"
done
su_ "$script" >/dev/null
if [ "$GPU_STYLE" = kgsl ]; then
# Collapse the pwrlevel range onto level 0. This also unlocks the top step: the devfreq
# governor's own max_freq sits one step below it, so a devfreq min_freq/max_freq pin cannot
# reach 1100 MHz at all.
su_ "echo $GPU_PIN_LEVEL > $KGSL/min_pwrlevel; echo $GPU_PIN_LEVEL > $KGSL/max_pwrlevel" >/dev/null
else
su_ "echo $GPU_PIN_LEVEL > $GPU_FIX_NODE" >/dev/null
fi
}
do_unpin() {
local spec label pol pin smin smax script=""
for spec in $POLICIES; do
IFS=: read -r label pol pin smin smax <<<"$spec"
script="$script echo \$(cat $CPUFREQ/$pol/cpuinfo_min_freq) > $CPUFREQ/$pol/scaling_min_freq;"
script="$script echo $smax > $CPUFREQ/$pol/scaling_max_freq;"
script="$script echo $smin > $CPUFREQ/$pol/scaling_min_freq;"
done
su_ "$script" >/dev/null
if [ "$GPU_STYLE" = kgsl ]; then
su_ "echo $GPU_STOCK_MIN_LEVEL > $KGSL/min_pwrlevel; echo $GPU_STOCK_MAX_LEVEL > $KGSL/max_pwrlevel" >/dev/null
else
su_ "echo -1 > $GPU_FIX_NODE" >/dev/null
fi
}
# Classify every node this script writes as at-pin or not-at-pin.
#
# NOT "at-pin / at-stock / neither". An earlier version compared against the hardcoded stock
# range and called anything else DRIFT, and ColorOS broke it within seconds: after an unpin
# restored policy4 to 300000-3500000, the Oppo performance daemon lowered the max to 3200000 on
# its own, and the next check reported DRIFT on a correctly released device. Stock maxima are
# daemon-managed and are NOT constants. So the only thing asserted here is our own pin - which
# is the question that actually matters ("is this run pinned?"). The hardcoded stock values
# still drive the restore path, where handing the range back to the governor is all they have
# to do.
do_check() {
local spec label pol pin smin smax n_pin=0 n_stock=0 n_other=0 drift="" notes=""
for spec in $POLICIES; do
IFS=: read -r label pol pin smin smax <<<"$spec"
local mn mx cu
mn=$(field ${label}_min); mx=$(field ${label}_max); cu=$(field ${label}_cur)
if [ "$mn" = "$pin" ] && [ "$mx" = "$pin" ]; then
n_pin=$((n_pin+1))
# min==max leaves the governor no room, so a cur that is not the pin means something
# outside cpufreq (thermal engine, vendor limiter) is overriding it.
[ "$cu" = "$pin" ] || drift="$drift ${label}(${pol}) pinned to $pin but scaling_cur_freq=$cu;"
elif [ "$mn" = "$mx" ]; then
# Clamped, but not by us. Someone else (game mode, thermal engine) is holding this policy
# at a fixed frequency, which is just as fatal to comparability as a missing pin.
n_other=$((n_other+1))
drift="$drift ${label}(${pol}) is clamped at $mn by something other than this script (our pin is $pin);"
else
n_stock=$((n_stock+1))
[ "$mn" = "$smin" ] && [ "$mx" = "$smax" ] || \
notes="$notes ${label}(${pol}) unpinned, range $mn-$mx (recorded stock $smin-$smax, which vendor daemons move);"
fi
done
if [ "$GPU_STYLE" = kgsl ]; then
if [ "$(field gpu_minlvl)" = "$GPU_PIN_LEVEL" ] && [ "$(field gpu_maxlvl)" = "$GPU_PIN_LEVEL" ]; then
n_pin=$((n_pin+1))
[ "$(field gpu_freq)" = "$GPU_PINNED_FREQ" ] || \
drift="$drift gpu pinned to pwrlevel $GPU_PIN_LEVEL but gpuclk=$(field gpu_freq) (expected $GPU_PINNED_FREQ);"
elif [ "$(field gpu_minlvl)" = "$GPU_STOCK_MIN_LEVEL" ] && [ "$(field gpu_maxlvl)" = "$GPU_STOCK_MAX_LEVEL" ]; then
n_stock=$((n_stock+1))
else
n_other=$((n_other+1))
drift="$drift gpu pwrlevel range $(field gpu_maxlvl)..$(field gpu_minlvl) is neither pin nor stock;"
fi
else
if [ "$(field gpu_fix)" = "$GPU_PIN_LEVEL" ]; then
n_pin=$((n_pin+1))
# The GPU parks its rail when idle and then reports a fallback frequency; only treat a
# mismatch as drift while the GPU is actually doing something.
if [ "$(field gpu_freq)" != "$GPU_PINNED_FREQ" ]; then
if [ "$(field gpu_busy)" = "0" ]; then
notes="$notes gpu fixed at OPP $GPU_PIN_LEVEL, freq reads $(field gpu_freq) with busy=0 - rail parked, not drift;"
else
drift="$drift gpu fixed at OPP $GPU_PIN_LEVEL but current_freqency=$(field gpu_freq) (expected $GPU_PINNED_FREQ) at busy=$(field gpu_busy)%;"
fi
fi
elif [ "$(field gpu_fix)" = "off" ]; then
n_stock=$((n_stock+1))
else
n_other=$((n_other+1))
drift="$drift gpu fix_target_opp_index='$(field gpu_fix)' is neither $GPU_PIN_LEVEL nor off;"
fi
fi
# Notes are commentary (a parked GPU rail is not a slipped pin) and are collected in their own
# variable rather than tagged inside $drift and filtered back out: the glob that would strip
# them is greedy, so it could swallow a real drift message that happened to follow one.
show() { [ -n "${1// /}" ] && echo "$1" | tr ';' '\n' | sed -e "s|^ *| $2|" -e "/^ *$2 *\$/d"; return 0; }
echo
if [ -n "${drift// /}" ]; then
echo " VERDICT: DRIFT"
show "$drift" "- "; show "$notes" "note: "
return 1
fi
if [ "$n_other" -gt 0 ] || { [ "$n_pin" -gt 0 ] && [ "$n_stock" -gt 0 ]; }; then
echo " VERDICT: DRIFT (partially pinned: $n_pin at pin, $n_stock at stock, $n_other neither)"
show "$notes" "note: "
return 1
fi
if [ "$n_pin" -gt 0 ] && [ "$n_stock" -eq 0 ]; then
echo " VERDICT: PINNED - all $n_pin pinned nodes at their pins, live frequencies match."
show "$notes" "note: "
return 0
fi
echo " VERDICT: UNPINNED - none of the $n_stock nodes is at a pin this script set; the vendor"
echo " governors have their range back and nothing this script writes is in effect."
show "$notes" "note: "
return 2
}
# --- main -------------------------------------------------------------------------------------
case "$ACTION" in
check)
load_state
echo "== check =="
print_state
do_check
exit $?
;;
pin)
load_state
echo "== before pin =="
print_state
do_pin
sleep 1
load_state
echo
echo "== after pin =="
print_state
do_check
rc=$?
[ "$rc" = 0 ] || echo " (pin did not take - do not measure against this)" >&2
exit $rc
;;
unpin)
load_state
echo "== before unpin =="
print_state
do_unpin
sleep 1
load_state
echo
echo "== after unpin =="
print_state
do_check
rc=$?
# 2 (UNPINNED) is success for this action.
[ "$rc" = 2 ] && exit 0
echo " (unpin did not fully restore - the device is still clamped)" >&2
exit 1
;;
esac