[Docs] (Performance): glmark2 optimization handoff with the landed fixes, current numbers, remaining work and the bench scripts

This commit is contained in:
2026-10-04 06:44:49 -04:00
parent c1e957cb98
commit 710beefd02
28 changed files with 695 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
# ab.sh <ct> <tag> <cmd...> - one quick run (phone, root), prints FPS lines
ct=$1; tag=$2; shift 2; CR=/mnt/Droidspaces/$ct/root; st=mgl; [ $ct = arch-kde ] && st=stock
sh /data/local/tmp/anl/pbcool.sh 46000 20 300 >/dev/null
mkdir -p $CR/pb; echo "$*" > $CR/pb/$tag.cmd
/data/local/Droidspaces/bin/droidspaces --name=$ct run bash /root/pbrun.sh $st $tag >/dev/null 2>&1
grep -h FPS $CR/pb/$tag.out | sed -e 's/FrameTime.*//' -e "s/^/$tag /"
@@ -0,0 +1,30 @@
import sys, re, subprocess, collections, os
f, tid, so, base, sub, target = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4],16), sys.argv[5], sys.argv[6]
SYM = os.path.expandvars(r'$LOCALAPPDATA/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-symbolizer.exe')
S=[]; cur=None
for line in open(f, encoding='utf-8', errors='replace'):
s=line.strip()
if s=='sample:':
if cur: S.append(cur)
cur={'ev':'','tid':'','fr':[]}; continue
if cur is None: continue
if s.startswith('event_type:'): cur['ev']=s.split()[1]
elif s.startswith('thread_id:'): cur['tid']=s.split()[1]
elif s.startswith('vaddr_in_file:'): cur['fr'].append([s.split()[1],''])
elif s.startswith('file:') and cur['fr']: cur['fr'][-1][1]=s.split(None,1)[1]
if cur: S.append(cur)
S=[x for x in S if x['tid']==tid and not x['ev'].startswith('sched')]
addrs=sorted({int(a,16)+base for x in S for a,d in x['fr'] if sub in d})
names={}
for i in range(0,len(addrs),3000):
part=addrs[i:i+3000]
out=subprocess.run([SYM,'--obj='+so,'-C','-f','--inlining=false']+['0x%x'%a for a in part],capture_output=True,text=True).stdout
for a,b in zip(part,out.strip().split('\n\n')): names[a]=re.sub(r'\(.*','',b.split('\n')[0]).split('::')[-1]
agg=collections.Counter(); tot=0
for x in S:
ch=[names.get(int(a,16)+base,'?') for a,d in x['fr'] if sub in d]
if target in ch:
k=ch.index(target); tot+=1
agg[' <- '.join(ch[k:k+4])]+=1
print('samples with', target, tot, 'of', len(S))
for k,v in agg.most_common(12): print(v, k)
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# dep.sh <DirectGLES|DirectVulkan> - (host) deploy a server (SERVER_LIB) + client (MGL_XBUILD_DIR) build and restart the desktop on that backend
S=${SKILL_SCRIPTS:-$(cd "$(dirname "$0")/../../../.claude/skills/anland-mobilegl-plasma/scripts" && pwd)}
# SERVER_LIB: the NDK-built server lib (Windows form path); MGL_XBUILD_DIR: the WSL client build dir.
export MSYS_NO_PATHCONV=1
adb push "${SERVER_LIB:?set SERVER_LIB}" /data/local/tmp/anland-mobilegl/libMobileGL.new.so >/dev/null
CHANNEL=pbxd bash $S/xdeploy.sh 2>&1 | grep -E "DEPLOYED|rror"
adb shell "su -c 'sh /data/local/tmp/anl/swap.sh >/dev/null; am startservice -n com.anland.consumer.mobilegl/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP >/dev/null; sleep 6; am force-stop com.anland.consumer.mobilegl; sleep 1; am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity --es mobilegl_backend $1 >/dev/null'"
sleep 40
printf '%s\n' 'sleep 2; pkill -x systemsettings; true' | bash $S/ct.sh pb >/dev/null 2>&1
adb shell 'su -c "sh /data/local/tmp/anl/status.sh 2>&1 | grep -E \"backend:|server|desktop-session\""'
@@ -0,0 +1,12 @@
# gprof.sh <ct> <tag> <rec_s> <cmd...> - offcpu+callgraph record of the busiest server apply thread + client
ct=$1; tag=$2; rec=$3; shift 3; D=/data/local/tmp/anl/prof; CR=/mnt/Droidspaces/$ct/root
echo "$*" > $CR/pb/$tag.cmd
/data/local/Droidspaces/bin/droidspaces --name=$ct run bash /root/pbrun.sh mgl $tag > $D/$tag.run 2>&1 &
sleep 9
s=$(pidof com.anland.consumer.mobilegl:mobilegl); c=$(pidof glmark2-es2-wayland glmark2-wayland | awk '{print $1}')
t=$(top -H -b -n1 -p $s -o TID,CMD,%CPU | grep mgl-srv-apply | sort -k3 -rn | head -1 | awk '{print $1}')
echo "server_tid=$t client=$c" > $D/$tag.pids
simpleperf record --trace-offcpu -e cpu-clock -f 2000 -g -t $t,$c --duration $rec -o $D/$tag.perf.data > $D/$tag.perf.log 2>&1
simpleperf report-sample -i $D/$tag.perf.data --show-callchain > $D/$tag.samples 2>/dev/null
chmod 644 $D/$tag.*
wait
+46
View File
@@ -0,0 +1,46 @@
# incl.py <samples> <tid> <so> <base> <dsosub> [top] - inclusive on-CPU %, and leaf-dso -> first own frame
import sys, re, subprocess, collections, os
f, tid, so, base, sub = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4], 16), sys.argv[5]
top = int(sys.argv[6]) if len(sys.argv) > 6 else 40
SYM = os.path.expandvars(r'$LOCALAPPDATA/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-symbolizer.exe')
S = []; cur = None
for line in open(f, encoding='utf-8', errors='replace'):
s = line.strip()
if s == 'sample:':
if cur: S.append(cur)
cur = {'ev': '', 'tid': '', 'fr': []}; continue
if cur is None: continue
if s.startswith('event_type:'): cur['ev'] = s.split()[1]
elif s.startswith('thread_id:'): cur['tid'] = s.split()[1]
elif s.startswith('vaddr_in_file:'): cur['fr'].append([s.split()[1], '', ''])
elif s.startswith('file:') and cur['fr']: cur['fr'][-1][1] = s.split(None, 1)[1]
elif s.startswith('symbol:') and cur['fr']: cur['fr'][-1][2] = s.split(None, 1)[1]
if cur: S.append(cur)
S = [x for x in S if x['tid'] == tid and not x['ev'].startswith('sched')]
addrs = sorted({int(a, 16) + base for x in S for a, d, _ in x['fr'] if sub in d})
names = {}
for i in range(0, len(addrs), 3000):
part = addrs[i:i+3000]
out = subprocess.run([SYM, '--obj=' + so, '-C', '-f', '--inlining=false'] + ['0x%x' % a for a in part], capture_output=True, text=True).stdout
for a, b in zip(part, out.strip().split('\n\n')):
n = re.sub(r'\(.*', '', b.split('\n')[0]).replace('MobileGL::', '').replace('MG_Backend::DirectVulkan::', 'DV::').replace('MG_Backend::DirectGLES::', 'DG::')
names[a] = n[:110]
def nm(fr):
a, d, sy = fr
if sub in d: return names.get(int(a, 16) + base, a)
return (sy if sy and not sy.startswith('*') and not sy.startswith('!!!') else os.path.basename(d))[:60]
N = len(S)
inc = collections.Counter(); leafmap = collections.Counter()
for x in S:
seen = set()
for fr in x['fr']:
if sub in fr[1]:
n = nm(fr)
if n not in seen: seen.add(n); inc[n] += 1
if x['fr'] and sub not in x['fr'][0][1]:
own = next((nm(fr) for fr in x['fr'] if sub in fr[1]), '?')
leafmap[(os.path.basename(x['fr'][0][1]) or '?', own)] += 1
print('on-CPU samples', N)
for k, v in inc.most_common(top): print('%5.1f%% %s' % (100.0*v/N, k))
print('-- foreign leaf -> first own frame')
for (d, o), v in leafmap.most_common(25): print('%5.1f%% %-22s %s' % (100.0*v/N, d[:22], o))
+4
View File
@@ -0,0 +1,4 @@
# lm.sh <tag> - per-frame link-metric averages of the last run's client log
L=/mnt/Droidspaces/arch-kde-mgl/root/pb/pb-$1.client.log
grep -a "kind=frame-op" $L | tail -1 | cut -c40-200
grep -a "P65LinkMetrics kind=frame " $L | tail -300 | awk '{for(i=1;i<=NF;i++){split($i,a,"="); v[a[1]]=a[2]} w+=v["wall_ns"]; c+=v["client_thread_cpu_ns"]; t+=v["transport_wait_ns"]; r+=v["rtt_mean_us"]; n++} END{printf "wall %.2fms cpu %.2fms twait %.2fms rtt %.0fus\n", w/n/1e6, c/n/1e6, t/n/1e6, r/n}'
@@ -0,0 +1,66 @@
import json, sys, statistics as st
d = json.load(open(sys.argv[1]))
stacks = ['stock', 'espryt', 'magma']
cfgs = [('es2w', 'glmark2-es2-wayland, 800x600 window'), ('es2f', 'glmark2-es2-wayland, fullscreen'),
('glw', 'glmark2-wayland (desktop GL), 800x600 window'), ('glf', 'glmark2-wayland (desktop GL), fullscreen')]
def med(v):
v = [x for x in v if x is not None]
return st.median(v) if v else None
def spread(v):
v = [x for x in v if x is not None]
return (min(v), max(v)) if v else (None, None)
out = []
out.append('### Overall scores (median of runs; min-max in brackets; n = runs)\n')
out.append('| Configuration | stock | Espryt | Magma | Espryt/stock | Magma/stock | Espryt/stock (common scenes) | Magma/stock (common scenes) |')
out.append('|---|---|---|---|---|---|---|---|')
for c, label in cfgs:
row = [label]
meds = {}
common = {}
for s in stacks:
k = '%s/%s' % (s, c)
if k not in d: row.append('-'); continue
sc = d[k]['scenes'].get('__score', [])
m = med(sc); lo, hi = spread(sc); meds[s] = m
row.append('%d [%d-%d] n=%d' % (m, lo, hi, len(sc)) if m else '-')
# common-scene mean of medians
keys = None
for s in stacks:
k = '%s/%s' % (s, c)
if k not in d: continue
ks = {n for n, v in d[k]['scenes'].items() if n != '__score' and med(v) is not None}
keys = ks if keys is None else keys & ks
for s in stacks:
k = '%s/%s' % (s, c)
if k in d and keys:
common[s] = st.mean(med(d[k]['scenes'][n]) for n in keys)
for s in ['espryt', 'magma']:
row.append('%.2f' % (meds[s] / meds['stock']) if meds.get(s) and meds.get('stock') else '-')
for s in ['espryt', 'magma']:
row.append('%.2f' % (common[s] / common['stock']) if common.get(s) and common.get('stock') else '-')
out.append('| ' + ' | '.join(row) + ' |')
out.append('')
for c, label in cfgs:
out.append('### Per-scene FPS: %s (median of runs)\n' % label)
out.append('| Scene | stock | Espryt | Magma | Espryt/stock | Magma/stock |')
out.append('|---|---|---|---|---|---|')
names = []
for s in stacks:
k = '%s/%s' % (s, c)
if k in d:
for n in d[k]['scenes']:
if n != '__score' and n not in names: names.append(n)
for n in names:
vals = {}
for s in stacks:
k = '%s/%s' % (s, c)
v = d.get(k, {}).get('scenes', {}).get(n)
vals[s] = med(v) if v else None
if v is not None and all(x is None for x in v): vals[s] = 'unsupported'
def f(x): return x if isinstance(x, str) else ('-' if x is None else '%d' % x)
def r(a, b):
return '%.2f' % (a / b) if isinstance(a, (int, float)) and isinstance(b, (int, float)) and b else '-'
out.append('| %s | %s | %s | %s | %s | %s |' % (n.replace('|', '/'), f(vals['stock']), f(vals['espryt']), f(vals['magma']),
r(vals['espryt'], vals['stock']), r(vals['magma'], vals['stock'])))
out.append('')
print('\n'.join(out))
@@ -0,0 +1,71 @@
# offcpu.py <samples.txt> <tid> <so> <base-hex> <dso-substr> [depth]
# Aggregates report-sample output (trace-offcpu) for one thread: on/off CPU split, and the
# off-CPU (leaf __schedule) time by kernel wait path + first N libMobileGL frames.
import sys, re, subprocess, collections, os
f, tid, so, base, sub = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4], 16), sys.argv[5]
depth = int(sys.argv[6]) if len(sys.argv) > 6 else 3
SYM = os.path.expandvars(r'$LOCALAPPDATA/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-symbolizer.exe')
samples = []
cur = None
mode = None
for line in open(f, encoding='utf-8', errors='replace'):
s = line.strip()
if s == 'sample:':
if cur: samples.append(cur)
cur = {'frames': [], 'count': 0, 'tid': None, 'time': 0, 'ev': ''}; mode = 'top'; continue
if cur is None: continue
if s.startswith('event_count:'): cur['count'] = int(s.split()[1])
elif s.startswith('time:'): cur['time'] = int(s.split()[1])
elif s.startswith('event_type:'): cur['ev'] = s.split()[1]
elif s.startswith('thread_id:'): cur['tid'] = s.split()[1]
elif s.startswith('vaddr_in_file:'): cur['frames'].append([s.split()[1], None, None])
elif s.startswith('file:') and cur['frames']: cur['frames'][-1][1] = s.split(None, 1)[1]
elif s.startswith('symbol:') and cur['frames']: cur['frames'][-1][2] = s.split(None, 1)[1]
if cur: samples.append(cur)
samples = sorted([x for x in samples if x['tid'] == tid], key=lambda x: x['time'])
for i, x in enumerate(samples):
if x['ev'].startswith('sched'):
x['count'] = (samples[i+1]['time'] - x['time']) if i + 1 < len(samples) else 0
x['off'] = True
else:
x['off'] = False
addrs = set()
for x in samples:
for a, d, sym in x['frames']:
if d and sub in d: addrs.add(int(a, 16) + base)
addrs = sorted(addrs)
names = {}
CH = 4000
for i in range(0, len(addrs), CH):
part = addrs[i:i+CH]
out = subprocess.run([SYM, '--obj=' + so, '-C', '-f', '--inlining=false'] + ['0x%x' % a for a in part], capture_output=True, text=True).stdout
for a, b in zip(part, out.strip().split('\n\n')):
n = b.split('\n')[0]
n = re.sub(r'\(.*', '', n) # drop args
n = n.replace('MobileGL::', '')
names[a] = n[:90]
def short(fr):
a, d, sym = fr
if d and sub in d: return names.get(int(a, 16) + base, a)
if sym and not sym.startswith('*'): return sym
return os.path.basename(d or '?')
tot = sum(x['count'] for x in samples)
off = [x for x in samples if x['off']]
offt = sum(x['count'] for x in off)
print('tid %s: total %.3fs, off-CPU %.3fs (%.0f%%)' % (tid, tot/1e9, offt/1e9, 100*offt/max(tot,1)))
agg = collections.Counter()
for x in off:
kern = [fr[2] for fr in x['frames'] if fr[1] and 'kernel' in fr[1]]
kpath = next((k for k in kern if any(w in k for w in ('futex','ep_poll','do_sys_poll','kgsl','unix_stream','sock','sync','fence','wait','nanosleep','read','sendmsg','recv'))), kern[1] if len(kern)>1 else '?')
user = [short(fr) for fr in x['frames'] if fr[1] and sub in fr[1]][:depth]
agg[(kpath, ' <- '.join(user))] += x['count']
for (k, u), v in agg.most_common(18):
print('%5.1f%% %-28s %s' % (100*v/max(tot,1), k[:28], u[:230]))
on = collections.Counter()
for x in samples:
if x['off']: continue
fr = x['frames'][0] if x['frames'] else ['?', '?', '?']
on[short(fr)] += x['count']
print('-- on-CPU top')
for k, v in on.most_common(15):
print('%5.1f%% %s' % (100*v/max(tot,1), k[:150]))
@@ -0,0 +1,28 @@
# pbbatch.sh <container> <stack stock|mgl> <label> <cfg...> (phone, root; run detached)
# cfg: es2w es2f glw glf ; results -> /data/local/tmp/anl/pb/<label>-<cfg>.*
ct=$1; st=$2; lab=$3; shift 3
D=/data/local/tmp/anl/pb; mkdir -p $D; DS=/data/local/Droidspaces/bin/droidspaces
CR=/mnt/Droidspaces/$ct/root
for cfg in "$@"; do
case $cfg in
es2w) cmd="glmark2-es2-wayland -b :duration=5";;
es2f) cmd="glmark2-es2-wayland --fullscreen -b :duration=5";;
glw) cmd="glmark2-wayland -b :duration=5";;
glf) cmd="glmark2-wayland --fullscreen -b :duration=5";;
es2o) cmd="glmark2-es2-wayland --off-screen -b :duration=5";;
glo) cmd="glmark2-wayland --off-screen -b :duration=5";;
*) cmd="$cfg";;
esac
tag=$lab-$cfg
[ $ct = arch-kde ] && sh $D/../stfix.sh > $D/$tag.fix 2>&1
sh $D/../pbcool.sh 46000 45 300 > $D/$tag.meta
echo "cmd: $cmd" >> $D/$tag.meta; echo "start $(date +%s)" >> $D/$tag.meta
sh $D/../pbsamp.sh $D/$tag.samp & sp=$!
rm -f $D/$tag.ended; sh $D/../pbprobe.sh $tag &
( sleep 25; screencap -p /data/local/tmp/anl/pb/$tag-s1.png; sleep 1; screencap -p /data/local/tmp/anl/pb/$tag-s2.png; md5sum $D/$tag-s1.png $D/$tag-s2.png >> $D/$tag.meta; stat -c "s1_bytes=%s" $D/$tag-s1.png >> $D/$tag.meta; sh $D/../pbshown.sh >> $D/$tag.meta ) &
mkdir -p $CR/pb; echo "$cmd" > $CR/pb/$tag.cmd
$DS --name=$ct run bash /root/pbrun.sh $st $tag >> $D/$tag.meta 2>&1
kill $sp; touch $D/$tag.ended; echo "end $(date +%s)" >> $D/$tag.meta; sleep 2
cp $CR/pb/$tag.out $D/ 2>/dev/null; cp $CR/pb/pb-$tag.client.log $D/ 2>/dev/null
done
echo DONE > $D/$lab.done
@@ -0,0 +1,26 @@
# pbclk.sh save|pin|restore|show (phone, root) - CPU/GPU clock pinning for the glmark2 baseline
O=/data/local/tmp/anl/pb-clocks.orig
C=/sys/devices/system/cpu/cpufreq; G=/sys/class/kgsl/kgsl-3d0
CPU_PIN="policy0:1804800 policy2:2246400 policy5:2246400 policy7:2246400"
GPU_PIN=680000000
case $1 in
save)
[ -f $O ] && { echo "already saved: $O"; cat $O; exit 0; }
for p in policy0 policy2 policy5 policy7; do echo "$p $(cat $C/$p/scaling_governor) $(cat $C/$p/scaling_min_freq) $(cat $C/$p/scaling_max_freq)"; done > $O
echo "gpu $(cat $G/devfreq/governor) $(cat $G/devfreq/min_freq) $(cat $G/devfreq/max_freq) $(cat $G/max_pwrlevel) $(cat $G/min_pwrlevel)" >> $O
cat $O;;
pin)
for e in $CPU_PIN; do p=${e%%:*}; f=${e##*:}; echo $f > $C/$p/scaling_max_freq; echo $f > $C/$p/scaling_min_freq; echo $f > $C/$p/scaling_max_freq; done
echo $GPU_PIN > $G/devfreq/max_freq; echo $GPU_PIN > $G/devfreq/min_freq; echo $GPU_PIN > $G/devfreq/max_freq
sh $0 show;;
restore)
[ -f $O ] || { echo "no saved state"; exit 1; }
while read p gov mn mx a b; do
if [ $p = gpu ]; then echo $mx > $G/devfreq/max_freq; echo $mn > $G/devfreq/min_freq; echo $mx > $G/devfreq/max_freq
else echo $gov > $C/$p/scaling_governor; echo $mx > $C/$p/scaling_max_freq; echo $mn > $C/$p/scaling_min_freq; echo $mx > $C/$p/scaling_max_freq; fi
done < $O
sh $0 show;;
show)
for p in policy0 policy2 policy5 policy7; do echo "$p gov=$(cat $C/$p/scaling_governor) min=$(cat $C/$p/scaling_min_freq) max=$(cat $C/$p/scaling_max_freq) cur=$(cat $C/$p/scaling_cur_freq)"; done
echo "gpu gov=$(cat $G/devfreq/governor) min=$(cat $G/devfreq/min_freq) max=$(cat $G/devfreq/max_freq) cur=$(cat $G/devfreq/cur_freq)";;
esac
@@ -0,0 +1,5 @@
# pbcool.sh [limit_mC=40000] [min_s=20] [max_s=600] - wait until CPU/GPU zones are below the limit
lim=${1:-40000}; mn=${2:-20}; mx=${3:-600}; t=0
hot() { m=0; for z in /sys/class/thermal/thermal_zone*; do case $(cat $z/type) in cpu-*|cpuss-*|gpuss-*) v=$(cat $z/temp); [ $v -gt $m ] && m=$v;; esac; done; echo $m; }
while :; do h=$(hot); if [ $t -ge $mn ] && [ $h -lt $lim ]; then break; fi; [ $t -ge $mx ] && break; sleep 5; t=$((t+5)); done
echo "cool: waited ${t}s maxtemp=$h batt=$(dumpsys battery | grep level | tr -d ' ')"
@@ -0,0 +1,4 @@
# pbkill.sh - stop every benchmark helper and glmark2 (phone, root)
for p in $(ps -A -o pid,args | grep -E "pbbatch|pbcool|pbsamp|stfix|profrun|pbprof" | grep -v grep | grep -v pbkill | awk '{print $1}'); do kill $p 2>/dev/null; done
pkill -x glmark2-es2-wayland; pkill -x glmark2-wayland; pkill -x glmark2-es2-way; pkill -x glmark2-wayland
sleep 1; ps -A -o pid,args | grep -E "pbbatch|glmark2" | grep -v grep
@@ -0,0 +1,33 @@
import re, sys, glob, os, statistics as st
# usage: pbparse.py <dir> ; files <round>-<stack>-<cfg>.out
d = sys.argv[1]
data = {} # (stack,cfg) -> list of dict scene->fps ; plus score
sizes = {}
for f in sorted(glob.glob(os.path.join(d, '*.out'))):
b = os.path.basename(f)[:-4]
m = re.match(r'(r\d+)-(\w+)-(\w+)$', b)
if not m: continue
rnd, stack, cfg = m.groups()
txt = open(f, encoding='utf-8', errors='replace').read()
sc = {}
for line in txt.splitlines():
mm = re.match(r'\[(\w+)\] (.*): FPS: (\d+)', line)
if mm: sc[mm.group(1) + ' ' + mm.group(2)] = int(mm.group(3))
mm = re.match(r'\[(\w+)\] (.*): Unsupported', line)
if mm: sc[mm.group(1) + ' ' + mm.group(2)] = None
mm = re.search(r'glmark2 Score: (\d+)', line)
if mm: sc['__score'] = int(mm.group(1))
mm = re.search(r'Surface Size:\s+(\S+ \S+)', line)
if mm: sizes[(stack, cfg)] = mm.group(1)
data.setdefault((stack, cfg), []).append((rnd, sc))
import json
out = {}
for k, runs in data.items():
scenes = {}
for rnd, sc in runs:
for s, v in sc.items(): scenes.setdefault(s, []).append(v)
out['%s/%s' % k] = {'size': sizes.get(k), 'runs': [r for r, _ in runs], 'scenes': scenes}
json.dump(out, open(os.path.join(d, 'all.json'), 'w'), indent=1)
for k in sorted(out):
s = out[k]['scenes'].get('__score', [])
print(k, out[k]['size'], 'scores', s, 'median', st.median(s) if s else None)
@@ -0,0 +1,10 @@
# pbprobe.sh <tag> - every 15 s while the run lasts: two screenshots 1 s apart; logs size and whether they differ
D=/data/local/tmp/anl/pb; tag=$1; n=0
sleep 10
while [ ! -f $D/$tag.ended ]; do
screencap -p $D/pr-a.png; sleep 1; screencap -p $D/pr-b.png
a=$(md5sum $D/pr-a.png | cut -c1-8); b=$(md5sum $D/pr-b.png | cut -c1-8); s=$(stat -c %s $D/pr-a.png)
st=ok; [ "$a" = "$b" ] && st=STATIC; [ $s -lt 200000 ] && st=BLACK
echo "probe t=$n size=$s $st" >> $D/$tag.meta
n=$((n+15)); sleep 13
done
@@ -0,0 +1,20 @@
# pbprof.sh <container> <tag> <warm_s> <rec_s> <cmd...> (phone, root)
# Runs <cmd> in the container (mgl stack via pbrun), then after warm_s records: per-thread CPU (top -H),
# simpleperf on the server (:mobilegl) and on the client, GPU busy samples. Results: /data/local/tmp/anl/prof/<tag>.*
ct=$1; tag=$2; warm=$3; rec=$4; shift 4
D=/data/local/tmp/anl/prof; mkdir -p $D; DS=/data/local/Droidspaces/bin/droidspaces; CR=/mnt/Droidspaces/$ct/root
st=mgl; [ $ct = arch-kde ] && st=stock
mkdir -p $CR/pb; echo "$*" > $CR/pb/$tag.cmd
$DS --name=$ct run bash /root/pbrun.sh $st $tag > $D/$tag.run 2>&1 &
sleep $warm
cli=$(pidof glmark2-es2-wayland glmark2-wayland | awk '{print $1}')
srv=$(pidof com.anland.consumer.mobilegl:mobilegl)
kw=$(pidof kwin_wayland | awk '{print $1}')
echo "client=$cli server=$srv kwin=$kw" > $D/$tag.pids
sh /data/local/tmp/anl/pbsamp.sh $D/$tag.samp & sp=$!
P=$cli; [ -n "$srv" ] && P=$P,$srv; [ -n "$kw" ] && P=$P,$kw
top -H -b -d $rec -n 2 -p $(echo $P | tr , ' ' | sed 's/ /,/g') -m 40 -o TID,PID,CMD,%CPU > $D/$tag.top 2>&1 &
simpleperf record -e cpu-clock -f 4000 -p $P --duration $rec -o $D/$tag.perf.data > $D/$tag.perf.log 2>&1
kill $sp
sh /data/local/tmp/anl/pbshown.sh >> $D/$tag.pids
wait
@@ -0,0 +1,13 @@
#!/bin/bash
# pbrun.sh <stack: stock|mgl> <tag> (container, root) - runs the command in /root/pb/<tag>.cmd
st=$1; tag=$2
mkdir -p /root/pb; out=/root/pb/$tag.out; cmd=$(cat /root/pb/$tag.cmd)
if [ "$st" = stock ]; then
sudo -u swung0x48 bash -c 'set -a; . /etc/environment; set +a; export XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus; cd /tmp; kscreen-doctor --dpms on >/dev/null 2>&1; eval "exec kde-inhibit --power --screenSaver $1"' x "$cmd" > $out 2>&1
else
rm -f /tmp/pb-$tag.client.log
MGLOG=/tmp/pb-$tag.log mgrun offscreen bash -c "$cmd" > $out 2>&1
grep -a -m1 "presented through" /tmp/pb-$tag.client.log >> $out
cp /tmp/pb-$tag.client.log /root/pb/ 2>/dev/null
fi
grep -a "Score" $out
@@ -0,0 +1,8 @@
# pbsamp.sh <outfile> - sample GPU freq/busy, temps, cpu7 freq every 2 s until killed
o=$1; : > $o
while :; do
g=$(cat /sys/class/kgsl/kgsl-3d0/devfreq/cur_freq); b=$(cat /sys/class/kgsl/kgsl-3d0/gpubusy)
m=0; gt=0; for z in /sys/class/thermal/thermal_zone*; do case $(cat $z/type) in cpu-*|cpuss-*) v=$(cat $z/temp); [ $v -gt $m ] && m=$v;; gpuss-*) v=$(cat $z/temp); [ $v -gt $gt ] && gt=$v;; esac; done
echo "$(date +%s) gpuf=$g busy=$b cpuT=$m gpuT=$gt c7=$(cat /sys/devices/system/cpu/cpu7/cpufreq/scaling_cur_freq) c5=$(cat /sys/devices/system/cpu/cpu5/cpufreq/scaling_cur_freq)" >> $o
sleep 2
done
@@ -0,0 +1,5 @@
# pbset.sh <tag> [extra env] - (phone) the affected-scene set, once, es2 window + texture fullscreen
t=$1; shift; E="$*"
cd /data/local/tmp/anl
sh ab.sh arch-kde-mgl $t-w $E glmark2-es2-wayland -b texture:duration=5 -b build:use-vbo=false:duration=5 -b build:use-vbo=true:duration=5 -b desktop:blur-radius=5:effect=blur:passes=1:separable=true:windows=4:duration=5 -b desktop:effect=shadow:windows=4:duration=5 -b ideas:speed=duration:duration=5 -b terrain:duration=5 -b refract:duration=5 -b jellyfish:duration=5 -b shadow:duration=5
sh ab.sh arch-kde-mgl $t-f $E glmark2-es2-wayland --fullscreen -b texture:duration=5 -b terrain:duration=5
@@ -0,0 +1,4 @@
# pbshown.sh - frames SurfaceFlinger latched for the anland SurfaceView in the last second (BLAST layer)
L=$(dumpsys SurfaceFlinger --list | grep -E "^SurfaceView\[com\.anland\.consumer.*\(BLAST\)#" | head -1)
[ -z "$L" ] && L=$(dumpsys SurfaceFlinger --list | grep -oE "SurfaceView\[com\.anland\.consumer[^]]*\]\(BLAST\)#[0-9]+" | head -1)
dumpsys SurfaceFlinger --latency "$L" | awk -v l="$L" 'NR>1 && $2>0 && $2<9e18 {t[n++]=$2; if($2>m)m=$2} END{c=0; for(i=0;i<n;i++) if(t[i]>m-1e9) c++; print "shown_last_1s=" c " layer=" l}'
@@ -0,0 +1,12 @@
# profrun.sh <container> <prefix> - profile the scene list (phone, root)
ct=$1; pre=$2; E="MOBILEGL_PIPE_STATS=1"; [ $ct = arch-kde ] && E=""
run() { t=$1; shift; sh /data/local/tmp/anl/pbcool.sh 46000 30 300 > /data/local/tmp/anl/prof/$pre-$t.cool; sh /data/local/tmp/anl/pbprof.sh $ct $pre-$t 7 8 "$@"; }
mkdir -p /data/local/tmp/anl/prof
run tex $E glmark2-es2-wayland -b texture:texture-filter=linear:duration=18
run texF $E glmark2-es2-wayland --fullscreen -b texture:texture-filter=linear:duration=18
run blur $E glmark2-es2-wayland -b desktop:blur-radius=5:effect=blur:passes=1:separable=true:windows=4:duration=18
run shad $E glmark2-es2-wayland -b desktop:effect=shadow:windows=4:duration=18
run ideas $E glmark2-es2-wayland -b ideas:speed=duration:duration=18
run terr $E glmark2-es2-wayland -b terrain:duration=18
run bvbo $E glmark2-es2-wayland -b build:use-vbo=false:duration=18
echo DONE > /data/local/tmp/anl/prof/$pre.done
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# smoke.sh <tag> - (host) Chrome scrolling + lock screen greeter + Plasma screenshot on the running MobileGL desktop
S=${SKILL_SCRIPTS:-$(cd "$(dirname "$0")/../../../.claude/skills/anland-mobilegl-plasma/scripts" && pwd)}
O=${OUT_DIR:-${TEMP:-/tmp}}
export MSYS_NO_PATHCONV=1
T=${1:-smk}
printf '%s\n' 'python3 -c "print(\"<html><body style=font-size:40px>\" + \"\".join(\"<p>line %d lorem ipsum dolor sit amet</p>\" % i for i in range(400)) + \"</body></html>\")" > /tmp/long.html; chmod 644 /tmp/long.html' | bash $S/ct.sh pb >/dev/null 2>&1
WAIT=15 bash $S/chrome-launch.sh $T file:///tmp/long.html 2>&1 | grep -E "presented|screenshot"
adb shell 'su -c "screencap -p /data/local/tmp/anl/sa.png; input mouse scroll 800 1200 --axis VSCROLL,-15; sleep 1; input mouse scroll 800 1200 --axis VSCROLL,-15; sleep 2; screencap -p /data/local/tmp/anl/sb.png; md5sum /data/local/tmp/anl/sa.png /data/local/tmp/anl/sb.png | cut -c1-12"'
printf '%s\n' 'U=$(id -u swung0x48); sudo -u swung0x48 env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus nohup /usr/lib/kscreenlocker_greet --testing > /tmp/greet.log 2>&1 & sleep 6; true' | bash $S/ct.sh pb >/dev/null 2>&1
adb shell 'su -c "screencap -p /data/local/tmp/anland-mobilegl/lock-'$T'.png"'; adb pull /data/local/tmp/anland-mobilegl/lock-$T.png $O/lock-$T.png >/dev/null
printf '%s\n' 'pkill -x kscreenlocker_greet; pkill -x chrome; true' | bash $S/ct.sh pb >/dev/null 2>&1
adb shell 'su -c "logcat -b crash -d -T 1 2>/dev/null | grep -c Abort; pidof com.anland.consumer.mobilegl:mobilegl"'
echo "lock screenshot: $O/lock-$T.png ; chrome: see the chrome-launch.sh line above"
@@ -0,0 +1,10 @@
# stfix.sh - (phone, root) if the stock desktop shows black, restart the stock stack; prints what it did
D=/data/local/tmp/anl/pb; DS=/data/local/Droidspaces/bin/droidspaces; C=/data/local/Droidspaces/Containers/arch-kde/container.config
screencap -p $D/pre.png; s=$(stat -c %s $D/pre.png)
if [ "$s" -lt 200000 ]; then
echo "stfix: black screen ($s bytes) - restarting the stock stack"
am force-stop com.anland.consumer; $DS -C $C stop >/dev/null 2>&1; sleep 2; $DS -C $C start >/dev/null 2>&1; sleep 3
monkey -p com.anland.consumer -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1; sleep 25
cp $D/../pbrun.sh /mnt/Droidspaces/arch-kde/root/pbrun.sh
screencap -p $D/pre.png; echo "stfix: now $(stat -c %s $D/pre.png) bytes"
else echo "stfix: screen ok ($s bytes)"; fi
@@ -0,0 +1,27 @@
# symagg.py <report.txt> <libname-substring> <so-path> <base-hex> [top]
# report.txt = simpleperf report --sort dso,vaddr_in_file output; aggregates samples per function.
import sys, re, subprocess, collections, os
rep, sub, so, base = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4], 16)
top = int(sys.argv[5]) if len(sys.argv) > 5 else 30
SYM = os.path.expandvars(r'$LOCALAPPDATA/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-symbolizer.exe')
rows = []
for line in open(rep, encoding='utf-8', errors='replace'):
m = re.match(r'\s*([\d.]+)%\s+(\S+)\s+(0x[0-9a-f]+|\S+)', line)
if not m: continue
rows.append((float(m.group(1)), m.group(2), m.group(3)))
addrs = [int(a, 16) + base for p, d, a in rows if sub in d and a.startswith('0x')]
names = {}
if addrs:
out = subprocess.run([SYM, '--obj=' + so, '-C', '-f', '--inlining=false'] + ['0x%x' % a for a in addrs], capture_output=True, text=True).stdout
blocks = [b for b in out.strip().split('\n\n')]
for a, b in zip(addrs, blocks):
names[a] = b.split('\n')[0][:140]
agg = collections.Counter()
for p, d, a in rows:
if sub in d and a.startswith('0x'):
agg[names.get(int(a, 16) + base, a)] += p
else:
key = os.path.basename(d) + (' ' + a if d.startswith('[kernel') or 'vdso' in d else '')
agg[key if not d.startswith('[kernel') else '[kernel]'] += p
for k, v in agg.most_common(top):
print('%6.2f%% %s' % (v, k))
@@ -0,0 +1,27 @@
import sys, collections
f = sys.argv[1]; tids = sys.argv[2].split(','); t0off = float(sys.argv[3]); span = float(sys.argv[4])
samples = []; cur = None
for line in open(f, encoding='utf-8', errors='replace'):
s = line.strip()
if s == 'sample:':
if cur: samples.append(cur)
cur = {'ev': '', 'tid': '', 'time': 0, 'syms': []}; continue
if cur is None: continue
if s.startswith('event_type:'): cur['ev'] = s.split()[1]
elif s.startswith('thread_id:'): cur['tid'] = s.split()[1]
elif s.startswith('time:'): cur['time'] = int(s.split()[1])
elif s.startswith('symbol:'): cur['syms'].append(s.split(None, 1)[1])
if cur: samples.append(cur)
samples = [x for x in samples if x['tid'] in tids]
samples.sort(key=lambda x: x['time'])
T0 = samples[0]['time'] + int(t0off * 1e6)
# collapse into intervals per tid: state on/off; print transitions
last = {}
for x in samples:
if x['time'] < T0 or x['time'] > T0 + span * 1e6: continue
st = 'OFF' if x['ev'].startswith('sched') else 'on '
k = [s for s in x['syms'] if 'kallsyms' not in s][:1]
key = (st, x['syms'][0] if st=='on ' else ','.join(s for s in x['syms'][1:12] if s in ('do_sys_poll','futex_wait_queue','unix_stream_read_generic','binder_thread_read','do_nanosleep','__wake_up_sync_key')))
if last.get(x['tid']) != key:
print('%9.3f ms %-6s %s %s' % ((x['time'] - T0) / 1e6, x['tid'], st, key[1][:60]))
last[x['tid']] = key
@@ -0,0 +1,9 @@
# visset.sh <tag> - screenshot several scenes mid-run (phone, root)
CR=/mnt/Droidspaces/arch-kde-mgl/root; D=/data/local/tmp/anland-mobilegl
for sc in ideas:duration=6 terrain:duration=6 shadow:duration=6 refract:duration=6 jellyfish:duration=6 desktop:effect=shadow:windows=4:duration=6 desktop:blur-radius=5:effect=blur:passes=1:separable=true:windows=4:duration=6 bump:bump-render=high-poly:duration=6 conditionals:duration=6 effect2d:duration=6; do
n=$(echo $sc | cut -d: -f1)
echo "glmark2-es2-wayland -b $sc" > $CR/pb/vis.cmd
/data/local/Droidspaces/bin/droidspaces --name=arch-kde-mgl run bash /root/pbrun.sh mgl vis >/dev/null 2>&1 &
sleep 4; screencap -p $D/$1-$n.png; wait
done
ls $D/$1-*.png | wc -l
@@ -0,0 +1,17 @@
#!/bin/bash
# wtest.sh build|run [ctest -R regex] - host unit tests for worktree $W in $B
W=${W:?set W to the MobileGL worktree as a WSL path}
B=${B:-$HOME/mgl-test-perf}
if [ "$1" = build ]; then
if [ ! -f $B/build.ninja ]; then
cmake -S $W -B $B -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_BUILD_STAMP=wsl > $B.cfg.log 2>&1 || { tail -20 $B.cfg.log; exit 1; }
fi
cd $B
T=$(ninja -t targets all 2>/dev/null | grep -oE "^[A-Za-z]+Test:" | tr -d : | sort -u | grep -v -E "^(DirectVulkan|Benchmark)" | tr "\n" " ")
ninja -j12 MobileGLServer $T > $B.build.log 2>&1; rc=$?
echo "build rc=$rc"; grep -E "error:|FAILED" $B.build.log | head -20
elif [ "$1" = run ]; then
cd $B; shift
ctest -L unit -j10 --timeout 300 -E "DirectVulkan" "$@" 2>&1 | tail -30
fi