Files
plezy/tools/perf/scenarios.mjs
edde746 f95465067e chore(tools): add the on-device frame and startup benchmark harness
The measurements behind the preceding commits needed a repeatable way to drive a
real Android TV box and read both sides of the frame. `dumpsys gfxinfo` alone is
not enough (it sees the HWUI composite, not Flutter's UI and raster threads), and
DevTools is not scriptable.

- `vmclient.mjs` -- Dart VM Service client over WebSocket, plus percentile and
  timeline helpers and a CPU self/total-time reducer over `getCpuSamples`.
- `scenarios.mjs` -- repeatable D-pad workloads. Keys are sent as one batched
  `input keyevent` invocation per burst, because a separate invocation per key
  costs more on the device than the interaction being measured.
- `bench.mjs` -- runs a scenario and correlates Flutter's own frame phases
  (`Animator::BeginFrame`, LAYOUT, SEMANTICS, BUILD, PAINT, `Rasterizer::DoDraw`)
  with HWUI framestats, reporting medians over repeats.
- `profile.mjs` -- CPU self/total time by function plus an allocation profile.
- `launch.sh` -- cold-launches the profile build, sets up its own port forward and
  prints a host-reachable VM Service URI. Passes `--ez enable-dart-profiling`,
  which only `flutter run` supplies by default, and wakes the display first
  because `am start -W` silently reports no `TotalTime` when the screen is off.

Not referenced by `lib/` and not packaged; `tools/` is outside the APK.
2026-08-23 18:13:39 +02:00

63 lines
1.8 KiB
JavaScript

// Repeatable D-pad workloads. One `input` invocation per burst (~30ms/key on
// the target box) so host-side adb latency does not dilute the device workload.
import { adb, sleep } from "./vmclient.mjs";
export const R = 22, L = 21, U = 19, D = 20, OK = 23, BACK = 4;
export const burst = (...codes) => adb("shell", "input", "keyevent", ...codes.map(String));
export const scenarios = {
// Stay inside the rail: 8 right, 8 left, repeated. Never dead-ends.
railRight: async () => {
for (let i = 0; i < 8; i++) {
burst(...Array(8).fill(R));
await sleep(700);
burst(...Array(8).fill(L));
await sleep(700);
}
},
railVertical: async () => {
for (let i = 0; i < 6; i++) {
burst(D, D, D, R, R, R);
await sleep(900);
burst(U, U, U, L, L, L);
await sleep(900);
}
},
detailRoundTrip: async () => {
for (let i = 0; i < 5; i++) {
burst(OK);
await sleep(3200);
burst(BACK);
await sleep(2200);
}
},
sidebar: async () => {
for (let i = 0; i < 8; i++) {
burst(L, L, L, L);
await sleep(600);
burst(R, R, R, R);
await sleep(600);
}
},
// Playback, chrome raised: exercises the 4 Hz position fan-out, the timeline
// slider repaint and the gradient scrim. Assumes a player is already running.
playbackChrome: async () => {
for (let i = 0; i < 6; i++) {
burst(U);
await sleep(2200);
}
},
// Held D-pad seek: the worst frame-time window on the TV path, because the
// key-repeat feedback and the decoder flush land in the same frames.
playbackSeek: async () => {
for (let i = 0; i < 6; i++) {
burst(...Array(8).fill(R));
await sleep(1400);
burst(...Array(8).fill(L));
await sleep(1400);
}
},
idle: async () => sleep(10000),
};