mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
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.
144 lines
8.2 KiB
Markdown
144 lines
8.2 KiB
Markdown
# `device_bench/p6` — the device A/B session runner
|
|
|
|
Two scripts that run a **paired performance comparison on a real device** and turn its evidence into
|
|
markdown tables. They exist because the quantity a disaggregation A/B reads on (tenths of a
|
|
millisecond of client CPU per frame) is smaller than the drift a hand-driven session accumulates
|
|
between the CPU samples, the pin evidence and the library's own counters — and those three come from
|
|
three different mechanisms. Collected by hand they are minutes apart; collected by one process they
|
|
are one window.
|
|
|
|
| file | role |
|
|
|---|---|
|
|
| `ab_session.py` | collects: reboot, pin, launch each arm in order, sample per-thread CPU, pull role logs, verify the arm |
|
|
| `render_ab.py` | renders: `session-summary.json` → the markdown tables a report needs |
|
|
|
|
The split is deliberate. The renderer reads only what the runner recorded, so it cannot influence
|
|
what was collected.
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
# One arm is TRANSPORT[:BACKEND[:CASE[:REPEATS]]]; repeat a spec to interleave it.
|
|
python tools/device_bench/p6/ab_session.py \
|
|
--tree /c/Users/me/mobilegl-snapshot \
|
|
--out /c/Users/me/evidence \
|
|
--apk /c/Users/me/evidence/trace-debug-signed.apk \
|
|
--package top.mobilegl.plugin.trace \
|
|
--reboot --fan-level 2 --repeats 3 --session my-session \
|
|
--arm monolith:DirectGLES:minecraft-1.21.4-rd12-odinlite-in-world:3 \
|
|
--arm inproc:DirectGLES:minecraft-1.21.4-rd12-odinlite-in-world:3 \
|
|
--arm spawn:DirectGLES:minecraft-1.21.4-rd12-odinlite-in-world:3
|
|
|
|
python tools/device_bench/p6/render_ab.py /c/Users/me/evidence/my-session/session-summary.json
|
|
```
|
|
|
|
`--tree` is the tree the APK and the runner come from. Point it at a **snapshot**, not the shared
|
|
tree, when anything else is editing the repository: a build that picks up a half-written source file
|
|
measures nothing, and the failure is invisible in the result. `--apk` must be signed with a key the
|
|
device already trusts for that package — the Gradle release build has no signing config locally
|
|
(`SIGNING_*` are CI secrets), so it has to be signed by hand first:
|
|
|
|
```bash
|
|
apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android \
|
|
--ks-key-alias androiddebugkey --key-pass pass:android --out signed.apk unsigned.apk
|
|
```
|
|
|
|
`--package` must match the APK's application id. A development build usually needs a suffix
|
|
(`-Pmobilegl.applicationIdSuffix=.mine`) so it can sit **beside** an existing install rather than
|
|
replacing it: two APKs signed by different keys cannot share an id, and `adb install -r` fails with
|
|
`INSTALL_FAILED_UPDATE_INCOMPATIBLE`. The runner then needs `MOBILEGL_TRACE_PACKAGE` set to the same
|
|
id (see `run_android_retrace_local.py`'s note beside `BACKENDS`), which `ab_session.py` does itself.
|
|
|
|
## What the protocol guarantees
|
|
|
|
The whole point of the script is that these are not optional:
|
|
|
|
- **Reboot-clean.** `--reboot` reboots, waits for `sys.boot_completed`, and compares the boot id
|
|
before and after. An unchanged id means no reboot happened, which is reported as a failure rather
|
|
than recorded as a clean window.
|
|
- **Pinned frequencies, with evidence.** Every arm runs `pin_device.sh <serial> pin` before and
|
|
`... check` after; a non-`PINNED` exit voids the arm (`status: UNPROVEN`), it does not degrade it.
|
|
Un-pinned per-thread CPU is not comparable across arms.
|
|
- **One thermal window.** All arms in one session, interleaved by the order the `--arm` flags are
|
|
given, with a cool-down between them so a later arm is not measured hotter than an earlier one.
|
|
- **Arm proof.** Each arm is checked against the library's OWN private log: the transport marker, the
|
|
split knobs in the `Config: IPC` line, and for `spawn` the `spawn ARMED - the server role runs in
|
|
pid N` line — which only the client writes after launch, connect and handshake all succeeded. A
|
|
spawn lane that silently fell back to monolith passes every other check and fails this one.
|
|
|
|
## Evidence layout
|
|
|
|
```
|
|
<out>/<session>/
|
|
session-summary.json # session metadata: boot ids, fan, APK hash, per-arm summaries
|
|
<NN>-<case>-<backend>-<transport>-r<N>/
|
|
arm-summary.json # this arm: status, problems, cpu, stats, benchmark runs
|
|
pin-before.txt pin-after.txt
|
|
mobilegl.client.log mobilegl.server.log # per role
|
|
benchmark-run1..N.json # per-frame wall + CPU series per repeat
|
|
benchmark.json result.json logcat.txt runner.stdout.txt runner.stderr.txt
|
|
```
|
|
|
|
### Two asymmetries of the `spawn` shape that the renderer handles
|
|
|
|
Both are structural — the client and the server really are different processes — and both produce a
|
|
zero that looks like a measurement if read carelessly:
|
|
|
|
1. **`srv`/`srvpark` live in the server log.** The apply thread publishes them into its own process's
|
|
PipeStats. Under `inproc` both roles are threads of one process, so the client's summary line
|
|
carries all four numbers; under `spawn` the client prints `srv=0 srvpark=0`, which means "another
|
|
process", not "never waited". `render_ab.py` reads the server pair from `mobilegl.server.log`.
|
|
|
|
2. **The frame count lives in the server log too.** `frames=` is bumped by `PipeStats::OnPresent`,
|
|
which the backend calls on Present — and under `spawn` present is applied in the server process,
|
|
so the client's `frames=0 window=0`. That zero is the same shape as `srv=0`. The per-frame rates
|
|
therefore divide by whichever log counted the frames, and the rendered table names which one that
|
|
was.
|
|
|
|
## Building the APK
|
|
|
|
```bash
|
|
gradle --no-daemon -p <tree>/android-plugin :app:assembleTraceRelease \
|
|
-Pmobilegl.buildDisaggregated=ON -Pmobilegl.buildDisaggregatedInproc=ON \
|
|
-Pmobilegl.pipePush=ON -Pmobilegl.debuggableRelease=true \
|
|
-Pmobilegl.applicationIdSuffix=.mine -Pmobilegl.apkSuffix=mine
|
|
```
|
|
|
|
`-Pmobilegl.debuggableRelease=true` is not optional: without it the release APK is not `run-as`-able,
|
|
`trace-replay-ci.sh` cannot pull the private logs, and every arm produces no library log — which is
|
|
indistinguishable from a run that produced none.
|
|
|
|
## Pitfalls this tooling works around
|
|
|
|
- **`--transport` is ignored in `--benchmark` mode.** `run_android_retrace_local.py` applies
|
|
`--transport` only on its `run_case` path; `run_benchmark_case` passes `args.env` alone. So
|
|
`--benchmark --transport spawn` runs monolith under a spawn label. This script passes the transport
|
|
as an explicit `--env MOBILEGL_TRANSPORT=…` and verifies it from the library's log.
|
|
- **`MOBILEGL_RETRACE_USE_PBUFFER` must be in the CI script's environment**, not passed as `--env` —
|
|
`trace-replay-ci.sh` reads it itself to decide the surface shape. Without it the `spawn` arm dies
|
|
with `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}`, because an `ANativeWindow*` means
|
|
nothing in the server's process (Rule H).
|
|
- **`MOBILEGL_TRACE_SKIP_INSTALL=1`** keeps the script's own `adb install` out of the window. The
|
|
session installs once itself and waits for `dex2oat` to exit first: an `am_kill … due to
|
|
installPackageLI` landing inside an arm measures the installer.
|
|
- **A stale `benchmark.json` is worse than none.** The runner's result directory is keyed by case and
|
|
backend, not by arm, so a failed arm can leave the previous arm's file where `read_benchmark` will
|
|
pick it up. The directory is deleted before each arm.
|
|
|
|
## Known limit
|
|
|
|
Under `spawn`, the client's **windowed** fields (`frames=`, `window=`, `draws/f`, `wrec/f`,
|
|
`bytes/f[…]`) are structurally zero: `PipeStats::OnPresent` runs in the server process (the apply
|
|
thread applies the present record), so the spawn client never counts frames. Dividing any counter
|
|
by the client's `frames=` is undefined. The renderer therefore takes the frame denominator from the
|
|
log that actually counted frames (normally the server's), and names the source in the table.
|
|
|
|
What is **valid** from the client log is the run-total gauges (`cli`, `clipark`, `maxrec`,
|
|
`ringwraps`, …): `PublishGauge` is a store, not an add, so the client's terminal `Shutdown()` line
|
|
reports true run totals. Symmetrically, the server log's `cli=0` and the client log's `srv=0` mean
|
|
"the other process", not "never waited".
|
|
|
|
The server's own byte/record/wait classes are available since `PipeStats::Init()` runs in
|
|
`ServerMain.cpp` (P6 closeout; before that the server log carried no summary line at all and `srv`
|
|
had to be reported as unavailable).
|