[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
+1 -1
View File
@@ -1,6 +1,6 @@
# glmark2 baseline: MobileGL (Espryt, Magma) vs the stock kgsl driver stack on anland
Measured 2026-10-04 on the anland Plasma desktop. This is the phase-1 baseline for the
Measured 2026-10-04 on the anland Plasma desktop. **What was optimized since, the current numbers and the remaining work: [glmark2-handoff.md](glmark2-handoff.md).** This is the phase-1 baseline for the
performance work: approximate numbers, how they were taken, where MobileGL loses, and a ranked list
of general fixes. The goal was "where does MobileGL fall short", not lab-grade accuracy, so the
scores are **approximate** (1-3 valid runs per configuration, see n in the tables); the profiling
+176
View File
@@ -0,0 +1,176 @@
# glmark2 performance work: handoff
Status on 2026-10-04 after the first optimization pass on top of the
[baseline](glmark2-baseline.md). Everything here was measured on the anland Plasma desktop of the
Lenovo TB321FU (Adreno 750) at **pinned clocks** (CPU 1.80 / 2.25 / 2.25 / 2.25 GHz, GPU 680 MHz;
see the baseline). Single runs or two at most per point, so the numbers are **approximate**
(light scenes vary about +-15% run to run). The tablet was in portrait for the later runs, so the
"after" fullscreen numbers are 1600x2560 and the "before" ones 2560x1600 (same pixel count).
## Headline
glmark2 score, `-b :duration=5`, default scene list:
| | stock kgsl stack | Espryt before | Espryt now | Magma before | Magma now |
|---|---|---|---|---|---|
| es2, 800x600 window | ~1076-1180 | ~1319 | **~1887** | ~778 | **~1213** |
| es2, fullscreen | ~1177 | ~572 | ~561 | ~354 | ~404 |
| desktop GL, 800x600 window | ~1161 | ~1283 | **~1809** | ~743 | **~1186** |
The "now" es2 window scores include the two `buffer update-method=map` scenes, which MobileGL now
runs (they were Unsupported before), so they are a fair comparison with stock's.
Per scene (es2 window unless marked F = fullscreen; FPS):
| scene | stock | Espryt before | Espryt now | Magma before | Magma now |
|---|---|---|---|---|---|
| texture | 1269 | 1672 | ~2210 | 915 | ~1580 |
| build use-vbo=false | 1095 | 467 | ~1215 | 472 | ~1040-1190 |
| build use-vbo=true | 1287 | 1675 | ~2450 | 1062 | ~1730 |
| desktop blur | 976 | 275 | **~1000** | 157 | ~680 |
| desktop shadow | 1044 | 234 | **~1000** | 125 | ~830 |
| ideas | 688 | 138 | ~550 | 66 | ~354 |
| terrain | 443 | 149 | ~220 | 115 | ~157 |
| shadow | 975 | 1139 | ~1840 | 644 | ~1170 |
| refract | 838 | 981 | ~1280 | 331 | ~520 |
| jellyfish | 1185 | 1438 | ~2170 | 841 | ~1155 |
| buffer map (2 scenes) | 330 / 408 | Unsupported | ~465 | Unsupported | ~431 |
| texture F | ~1430 | ~726 | ~597 | ~445 | ~398 |
| terrain F | - | - | ~142 | - | ~112 |
**Remaining gap:** Espryt is at or above stock in every windowed scene except **ideas (0.8x)** and
**terrain (0.5x)**. Magma is behind on most draw-heavy scenes (ideas 0.5x, terrain 0.35x, desktop
0.7-0.8x, refract 0.6x). **Fullscreen** is the biggest gap on both backends (0.4-0.5x of stock),
and nothing landed here addresses it.
## Done (all on origin/feat/disaggregated)
| commit | change | why / evidence | effect (approx) |
|---|---|---|---|
| `4722fa38` | the baseline report | - | - |
| `66b47a10` | **glFinish waits for the GPU** on the remote path (a server fence created, waited with `GL_SYNC_FLUSH_COMMANDS_BIT`, destroyed); **Magma submits** once 1024 retired per-clear objects pile up with nothing submitted; **ES contexts advertise** `GL_OES_rgb8_rgba8`, `GL_OES_depth24`, `GL_OES_mapbuffer` (with `glMapBufferOES` / `glUnmapBufferOES` / `glGetBufferPointervOES` exported as the core calls) | glFinish only waited for the server's apply, so off-screen rendering ran unbounded (glmark2 `--off-screen` "6846" / "12117" fps) and drove Magma's server to `VK_ERROR_OUT_OF_HOST_MEMORY` in `ClearWireFramebuffer`; es2 `--off-screen` could not start (glmark2 picked `GL_RGBA4` and no depth) | off-screen now real numbers (Espryt texture ~866, Magma ~800) and no crash; es2 `--off-screen` runs; the two buffer-map scenes run |
| `2de0d6dd` | **client-array buffers are reused** from a per-share-group pool (respecify, a non-blocking record, instead of a blocking ResourceCreate + destroy per array per draw); contiguous element ranges copied in one memcpy; the fetch plan skips sorting an already ascending range | desktop blur/shadow paid 28/52 blocking creates per frame, build use-vbo=false 2; `std::sort` was 27% of the client in build use-vbo=false | build use-vbo=false Espryt 467 -> ~1100, Magma 472 -> ~1040; blocking replies per frame in desktop 29/53 -> 1 (fps of desktop barely moved at this step: the next item was the limit) |
| `d627fca3` | **targeted wake-ups:** the client publishes the applied sequence it waits for (`RingControl::producerWakeSeq`, its own cache line); the server rings a parked client only once that sequence is applied (or the event ring filled); the parked flag is stored with release so the notifier sees the target | while the client waited for its present reply the server rang it after every record it applied: 18% of Espryt's apply thread was `SocketDoorbell::Notify`, and every ring was a futile wake-up and re-park on the client | Espryt desktop ~250 -> ~800, ideas 138 -> ~380, texture 1672 -> ~2190; Magma desktop ~150 -> ~550-650, ideas 45 -> ~137, refract 372 -> ~540 |
| `963f4d81` | Magma **reuses completed command buffers** (pool resets them at the next begin) instead of `vkFreeCommandBuffers` + `vkAllocateCommandBuffers` per submission | `FreeRetiredCommandBuffersCompletedUpTo` was 15% of Magma's apply thread in ideas | small: Magma texture / desktop +~10% |
| `915882b2` | the server **times records only when** `MOBILEGL_SERVER_FRAME_STATS` is on | two `steady_clock::now()` per record were ~5% of Espryt's apply thread | with the next row |
| `e5ee31b0` | the anland server build uses **`android-30`** (the APK's minSdk) in `build-android.sh` / `reproduce.md`: `thread_local` is native ELF TLS instead of emulated TLS | `__emutls_get_address` + `pthread_getspecific` were 7-12% of Espryt's apply thread. **An existing build dir keeps its API level: reconfigure (delete it) to get android-30** | Espryt (with the row above) desktop ~800 -> ~1000, ideas ~380 -> ~550, terrain 168 -> ~220; Magma unchanged |
| `bb31f374` | Magma **keeps the wire draw's render pass open** across draws to the same attachments. Every command that may not sit inside a pass (barriers, copies, clears, blits, dispatches, other passes' begins) in the resource managers and wire helpers first calls `EndActiveRenderPassOn(cb)` (`RenderPassGuard.h`); the per-draw "make image writes visible" barrier for sampled images is only recorded when an image may have been written since the last one (`WireImageWriteEpoch`, bumped by every pass end, out-of-pass work and storage-image draw); same-layout attachment barriers are skipped when the pass continues | `SetupWireDraw` ended and began a VkRenderPass for every draw (ideas: 305 passes per frame; End/Begin ~21% of the apply thread, plus `vkEndCommandBuffer` cost), and on a tiler each pass loads and stores its attachments | Magma ideas ~146 -> ~354, desktop shadow ~640 -> ~830; visually checked on ideas, terrain, shadow, refract, jellyfish, desktop, bump, conditionals, effect2d, Plasma, Chrome and the lock screen |
Each landed with the host unit tests green (WSL clang, `ctest -L unit -E DirectVulkan`, 2851
tests) and a device check on both backends (frames shown, Plasma, Chrome page + scroll input,
`kscreenlocker_greet --testing`).
## Findings
- **The common shape before the fixes: nothing was saturated.** GPU 7-40% busy, client and server
threads each ~30-50% busy, both waiting for each other with a wake-up per handoff. Removing
futile wake-ups (`d627fca3`) was worth more than any per-draw CPU saving.
- **Espryt after the fixes:** the remaining windowed gaps are ideas and terrain. Its present reply
is still a synchronous round trip per frame, and the server's per-draw state sync
(`DG::PrepareForDraw`, 28% of the apply thread in ideas before) is now the larger share.
- **Magma after the fixes:** CPU-bound on the server. Remaining per-draw costs in
`SetupWireDraw` (descriptor sets, `GetOrCreateProgram` / pipeline hash, `BuildWireVertexInput`,
vectors allocated per draw, scudo lock contention ~20% before), and per frame
`BlitDefaultFramebufferToSharedImage` (~30% of the apply thread: frame flush + a second submit
for the copy + `vkEndCommandBuffer`) plus `Present()` of the client's offscreen default surface
(a swapchain acquire + submit that nobody looks at) and GPU progress markers per submit.
- **Fullscreen:** every frame is rendered into the server's default framebuffer and then copied
into the window's shared image (Espryt `glBlitFramebuffer`, Magma `vkCmdCopyImage`); at 4 MP the
GPU is ~98% busy on texture at ~600 fps while stock does ~1430 and is even faster fullscreen than
windowed (it presumably skips composition).
**Dead end: an adaptive client spin.** Growing the doorbell spin up to 1 ms towards the measured
reply latency (and shrinking it on long waits) gave +30-80% on light scenes (texture, jellyfish,
shadow) on Magma but cost refract -35% (370 -> 236) and fullscreen terrain -35% (90 -> 58),
reproducibly; the present reply simply got slower while the client spun. Polling the shared line
less often (backoff up to 64 iterations) did not change that, so it is not cache-line ping-pong. A
static `MOBILEGL_IPC_SPIN_US=1000` on the client showed the same mixed picture on Espryt. Not
landed; the targeted wake-up removed most of what the spin bought without the regressions.
**Traps:**
- **Pin the clocks.** Unpinned, the stock stack's light scenes ran at GPU 310 MHz / big cores 672
MHz and scored 425 instead of ~1100.
- **glFinish and `--off-screen`.** Before `66b47a10` off-screen MobileGL numbers were meaningless.
- **The stock desktop turns black mid-run** (a pure-black 21707-byte screenshot) while
SurfaceFlinger keeps latching ~125 buffers/s, and its score then jumps to ~1420-1500. Probe the
screen every ~15 s during stock runs and discard runs with a black probe; restart the stock
container + app (`stfix.sh`) to get the picture back. A powerdevil inhibit did not prevent it.
- **The MobileGL app's extra-keys bar** shrinks its desktop to ~2560x1410; set
`extra_keys_mode=with_keyboard` for fullscreen comparisons (it was restored afterwards).
- **The MobileGL session restores a System Settings window** at login; close it before measuring.
- **Rotation:** the stock app follows auto-rotate; check glmark2's "Surface Size" line.
- **Symbolizing:** the APK's server lib is stripped; a RelWithDebInfo build of the same tree was at
+0x80 for every function (check with `llvm-nm -D` on both). Container clients: simpleperf prints
offsets from the start of the executable mapping (add the r-x segment's page-aligned vaddr,
0x4fb000 for the lib of 2026-10-04). Off-CPU (`sched_switch`) samples of container processes
get their user frames attributed to Android's libs, so use the client's own `P65LinkMetrics` for
its waits.
- `pkill -f <script>` inside `su -c "..."` kills the command itself; use pids.
## Yet to do (ranked)
1. **Fullscreen: render straight into the window's shared images** (both backends). The default
framebuffer becomes the current back shared image (an AHB-backed EGLImage FBO on Espryt, the
imported image on Magma); the bottom-up row order goes to linux-dmabuf's `Y_INVERT` flag instead
of the flipping blit. Gain: fullscreen ~1.5-2x. Risk: default-framebuffer semantics (reads,
blits, buffer age, resize, device loss), KWin's handling of `Y_INVERT`. Verify: fullscreen
glmark2, Chrome, desktop-verify, the retrace CI.
2. **Asynchronous shared-image present.** Today `PresentToSharedImage` is `EmitAndWait`, so the
client cannot record frame N+1 while the server finishes N. Commit the `wl_buffer` from the reply
on the event ring (or order the compositor's acquire after the producer's present server-side),
and allow present credit 2. Gain: light scenes 1.3-1.8x. Risk: frame ordering with the
compositor, buffer reuse with 3 images, an app that draws one frame and idles. Verify: Chrome
flicker barcode test, KWin, desktop-verify.
3. **Magma per-frame submissions:** append the shared-image copy to the frame's command buffer (one
submit per frame), skip `Present()` for a surface whose frames only go to shared images, coalesce
GPU progress markers. Gain: Magma light scenes 1.2-1.4x. Risk: the copy's fence/semaphore
export (`SI::PublishWrite`), hang-watch coverage. Verify: Magma unit tests, shared-image tests.
4. **Magma per-draw CPU:** cache descriptor sets / pipeline lookups across draws when unchanged
(keyed on monotonic lifetime ids), stop rebuilding the vertex input state per draw, replace the
per-draw vectors with per-frame arenas. Gain: ideas/terrain 1.3-2x. Verify: retrace CI with
Magma.
5. **Terrain** (both backends at 0.35-0.5x of stock): not profiled after the fixes; it has 43
respecifies, several FBO passes and a GenerateMipmap per frame. Profile first.
6. **Espryt per-draw state sync** (`DG::PrepareForDraw`): the Minecraft roadmap's version-gated
early-outs. Gain 10-20% on draw-heavy scenes. Risk: stale caches; retrace CI.
7. **Blocking fence create/destroy:** `EmitFenceSync` / `EmitDeleteSync` are `EmitAndWait`; an app
that makes a fence per frame (Minecraft, Chrome) pays two round trips. Make them fire-and-forget
(the client already mints the handle).
8. **Client-to-server doorbell:** the server parks between records and the client pays a socket
write per record while it streams. Try a server-side spin sized to the inter-record gap of an
actively streaming client (watch the spin regression above), or ring only at batch boundaries.
9. `CreateVertexElements` is re-sent per draw (ideas: 76 per frame); deduplicate by content on the
client.
## How to reproduce
Helper scripts: `tools/device_bench/anland_glmark2/` (copied verbatim from the session; the phone
side expects them in `/data/local/tmp/anl/`, push with `adb push <file> /data/local/tmp/anl/`).
| script | where | does |
|---|---|---|
| `pbclk.sh save\|pin\|restore\|show` | phone, root | saves the governors/limits to `/data/local/tmp/anl/pb-clocks.orig`, pins CPU 1.80/2.25/2.25/2.25 GHz + GPU 680 MHz, restores |
| `pbbatch.sh <container> <stock\|mgl> <label> <cfg...>` | phone, root | per config (`es2w es2f glw glf es2o glo`): cool-down (`pbcool.sh`), samples (`pbsamp.sh`), screenshots + SurfaceFlinger frames (`pbshown.sh`), 15 s screen probes (`pbprobe.sh`), runs glmark2 through `pbrun.sh` in the container (`/root/pbrun.sh`), black-screen restart for stock (`stfix.sh`) |
| `ab.sh <container> <tag> <cmd...>` / `pbset.sh <tag> [env]` | phone, root | one run / the affected-scene set |
| `lm.sh <tag>` | phone, root | per-frame `P65LinkMetrics` averages of a run made with `MOBILEGL_PIPE_STATS=1` |
| `pbprof.sh`, `profrun.sh`, `gprof.sh` | phone, root | `simpleperf` (flat, per-thread `top -H`, GPU busy) and `--trace-offcpu -g` call graphs of the server apply thread + client |
| `visset.sh <tag>` | phone, root | mid-run screenshots of ten scenes |
| `dep.sh`, `smoke.sh`, `wtest.sh` | host | deploy a server + client build and restart on a backend; Chrome + lock screen smoke check; WSL unit tests |
| `pbparse.py`, `mktables.py` | host | glmark2 outputs -> per-scene tables |
| `symagg.py`, `incl.py`, `offcpu.py`, `callers.py`, `timeline.py` | host | symbolize and aggregate simpleperf output (flat by function, inclusive call graph, off-CPU waits, callers of a function, on/off-CPU timeline) |
```sh
# clocks (phone, root)
sh /data/local/tmp/anl/pbclk.sh save; sh /data/local/tmp/anl/pbclk.sh pin
sh /data/local/tmp/anl/pbclk.sh restore # when done (also on abort)
# backends / stacks
sh /data/local/tmp/anl/switch.sh DirectGLES|DirectVulkan # MobileGL anland (skill script)
am force-stop com.anland.consumer.mobilegl; droidspaces -C /data/local/Droidspaces/Containers/arch-kde/container.config start
monkey -p com.anland.consumer -c android.intent.category.LAUNCHER 1 # stock
# one glmark2 run in the MobileGL container (root shell in the container)
MGLOG=/tmp/pb-x.log mgrun offscreen bash -c 'MOBILEGL_PIPE_STATS=1 glmark2-es2-wayland -b ideas:duration=10'
# profile the busiest server apply thread + the client
simpleperf record --trace-offcpu -e cpu-clock -f 2000 -g -t <apply tid>,<client pid> --duration 6 -o x.data
simpleperf report-sample -i x.data --show-callchain > x.samples
python incl.py x.samples <tid> <unstripped libMobileGL.so> <offset> com.anland.consumer.mobilegl
```
+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