charts: scope per device; add low-end (Redmi A5) view

run_environment.csv is keyed by (build, device) but the env lookup was keyed by
build alone — the Redmi rows overwrote the A36's and the gate scorecard lost its
2.38.2 baseline on every surface. Same hole in the per-surface charts: a surface
naming no device charted all devices (21 Redmi points on 8 A36 charts). Both now
scope to one phone; unnamed = the gate phone.

Adds a low-end scorecard, an A36-vs-Redmi comparison and per-surface Redmi trends
under docs/android/lowend/, published apart from the gate — the gate stays one
reference phone by design.

Gate verdicts are also withheld when only one side is below the measurement floor
(2.38.2 read Buy and Home at ~0.1s, so a normal 0.6s printed as "+0.5s slower").

Assisted-by: Claude Opus 5 (Anthropic)
This commit is contained in:
glitchminer
2026-08-10 16:49:42 +01:00
parent 45eb8e1f94
commit 07ea877c28
20 changed files with 48 additions and 27 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+48 -27
View File
@@ -30,6 +30,17 @@ plt.rcParams.update({
PERFORMANCE_COLORS = ['#10AC84', '#2E86DE', '#F79F1F', '#54A0FF']
# The reference phone. A surface that names no device is charted on this one alone — the
# store is multi-device, and a second phone's rows plotted onto the same line would read as
# build-to-build movement.
GATE_DEVICE = "SM-A366B"
LOWEND_DEVICE = "25028RN03Y" # Redmi A5 — the low-end baseline, charted separately
LOWEND_NAME = "Redmi A5"
# OS regime each phone's published series is pinned to, so an OS update cannot silently
# re-baseline a chart. Unlisted phone = unpinned (all its points chart).
CHART_REGIME = {GATE_DEVICE: "16", "moto_g55_5G": "16", LOWEND_DEVICE: "15"}
@dataclass
class PerformanceTest:
@@ -95,39 +106,46 @@ def _excluded_builds():
return out
def _run_environments():
def _env_rows():
import csv as _csv
p = Path(__file__).resolve().parent.parent / 'data' / 'android' / 'run_environment.csv'
return list(_csv.DictReader(open(p, encoding='utf-8'))) if p.exists() else []
def _run_environments(device=GATE_DEVICE):
"""commit_hash -> device OS fingerprint (data/android/run_environment.csv).
Used to draw a divider where the device software changed, so a baseline step
that is really an OS/One UI update isn't misread as an app change. Builds with
no recorded environment are treated as one earlier 'legacy' regime."""
import csv as _csv
p = Path(__file__).resolve().parent.parent / 'data' / 'android' / 'run_environment.csv'
no recorded environment are treated as one earlier 'legacy' regime.
The file is keyed by (build, device), so a caller MUST scope to one phone: when a
second phone measures the same build its row overwrites the first's, and every
same-OS baseline lookup for that build then misses. Pass device=None only to read
the raw last-wins map."""
out = {}
if p.exists():
for row in _csv.DictReader(open(p, encoding='utf-8')):
out[row['commit_hash']] = row.get('fingerprint') or row.get('oneui') or 'recorded'
for row in _env_rows():
if device and row.get('device') != device:
continue
out[row['commit_hash']] = row.get('fingerprint') or row.get('oneui') or 'recorded'
return out
def _android16_builds():
"""Build hashes measured on the gate OS regime — Android 16 (One UI 8). Volo asked to
'stick with Android 16 One UI 8', so the trend charts show only this regime: it drops the
pre-update legacy points and the OS-divider clutter, and keeps one comparable timeline. A
build with no recorded environment is treated as pre-regime and left off."""
import csv as _csv
p = Path(__file__).resolve().parent.parent / 'data' / 'android' / 'run_environment.csv'
out = set()
if p.exists():
for row in _csv.DictReader(open(p, encoding='utf-8')):
if str(row.get('android', '')).strip() == '16':
out.add(row['commit_hash'])
return out
def _regime_builds(device):
"""Build hashes measured on `device` in the OS regime its published series is pinned to
(CHART_REGIME). Volo asked the gate charts to 'stick with Android 16 One UI 8', so points
from an older OS are dropped rather than plotted as an app change. An unpinned phone
returns the empty set, which callers read as 'chart everything it has'."""
want = CHART_REGIME.get(device)
if not want:
return set()
return {r['commit_hash'] for r in _env_rows()
if r.get('device') == device and str(r.get('android', '')).strip() == want}
def _os_boundary_indices(order):
def _os_boundary_indices(order, device=GATE_DEVICE):
"""Indices i (in the date-ordered build list) where the device OS regime changes
from build i-1 to build i — i.e. where to draw a 'device OS update' divider."""
env = _run_environments()
env = _run_environments(device)
regimes = [env.get(h, 'legacy') for h in order['commit_hash']]
return [i for i in range(1, len(regimes)) if regimes[i] != regimes[i - 1]]
@@ -152,14 +170,15 @@ def plot_performance_mobile(performance, test, output_dir):
data = performance[name_match].copy()
if 'metric' in data.columns:
data = data[data['metric'] == test.series]
if test.device and 'device' in data.columns:
data = data[data['device'] == test.device]
device = test.device or GATE_DEVICE
if 'device' in data.columns:
data = data[data['device'] == device]
excluded = _excluded_builds()
if excluded:
data = data[~data['commit_hash'].isin(excluded)]
a16 = _android16_builds() # Volo: Android 16 / One UI 8 only — drop legacy-OS points
if a16:
data = data[data['commit_hash'].isin(a16)]
regime = _regime_builds(device)
if regime:
data = data[data['commit_hash'].isin(regime)]
if data.empty:
print(f"Warning: No data for {test.pattern}")
return
@@ -188,6 +207,8 @@ def plot_performance_mobile(performance, test, output_dir):
fo = performance[performance['test_name'] == test.pattern.replace('_response_time', '_first_open')].copy()
if 'metric' in fo.columns:
fo = fo[fo['metric'] == 'response_time']
if 'device' in fo.columns:
fo = fo[fo['device'] == device]
fo = fo[fo['commit_hash'].isin(order_hashes)].copy()
fo['_pos'] = fo['commit_hash'].map(build_index)
fo = fo.sort_values('_pos')