Skip to content

Commit 0a6cc72

Browse files
committed
perf(harness): warm the runner after open so the first measured command is clean
The first interaction after open/relaunch pays the one-time iOS XCUITest runner startup (~10s+ cold) and a per-relaunch first-AX-query settle cost (~4s). That was landing on the first measured command each round (snapshot -i), inflating it ~10x vs the next snapshot. Run an untimed warmup snapshot -i after establishSession, after each round's reset-open, and after every freshRoot relaunch, so no measured command absorbs runner startup. Noted in the report header.
1 parent 7f154d2 commit 0a6cc72

2 files changed

Lines changed: 17 additions & 0 deletions

File tree

scripts/perf/harness.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,18 @@ function sampleError(r: ReturnType<typeof invokeCli>): Pick<Sample, 'errorCode'
6565
};
6666
}
6767

68+
// The first interaction after open/relaunch pays the iOS XCUITest runner startup (~10s+ cold)
69+
// and a per-relaunch first-AX-query settle cost. Run an untimed throwaway interaction so that
70+
// cost is never attributed to a measured command.
71+
function warmRunner(ctx: IsolationContext): void {
72+
invokeCli(['snapshot', '-i'], ctx.baseFlags);
73+
}
74+
6875
function runStep(step: ScenarioStep, ctx: IsolationContext, round: number): Sample {
6976
// Untimed reset to root for steps whose precondition is a clean, top-of-list root.
7077
if (step.freshRoot) {
7178
invokeCli(['open', ctx.profile.appTarget, '--relaunch'], ctx.baseFlags);
79+
warmRunner(ctx);
7280
}
7381
const r =
7482
step.execMode === 'standalone'
@@ -151,6 +159,8 @@ export function runScenario(ctx: IsolationContext, cfg: PerfConfig): Measurement
151159

152160
const boot = bootOnce(ctx);
153161
const establish = establishSession(ctx);
162+
// Absorb the one-time runner startup before any round so it isn't charged to a measurement.
163+
warmRunner(ctx);
154164

155165
// Android accessibility dumps time out while the UI is animating; disable animations
156166
// up front (untimed) so snapshot/get/is/fill can read an idle hierarchy.
@@ -166,6 +176,11 @@ export function runScenario(ctx: IsolationContext, cfg: PerfConfig): Measurement
166176
for (const step of steps) {
167177
const sample = runStep(step, ctx, round);
168178
if (measured) acc.get(step.label)!.push(sample);
179+
// After the round's reset-open relaunch, warm the runner (untimed) so the first measured
180+
// read (snapshot -i) doesn't pay the post-relaunch first-AX-query cost.
181+
if (step.command === 'open' && step.execMode === 'standalone') {
182+
warmRunner(ctx);
183+
}
169184
}
170185
}
171186

scripts/perf/report.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ function toMarkdown(run: RunResult): string {
3535
lines.push('All times in milliseconds. `wall-clock` includes process spawn + socket overhead;');
3636
lines.push('`daemon` is the batch step round-trip (spawn overhead ≈ wall-median − daemon-median).');
3737
lines.push('`elements` = distinct interactive @eN refs in the snapshot payload (tree-size proxy).');
38+
lines.push('An untimed warmup interaction runs after each open/relaunch, so measured commands');
39+
lines.push('do not pay the one-time iOS-runner startup or post-relaunch first-AX-query cost.');
3840
lines.push('');
3941
lines.push('| command | cli | mode | n | wall min | wall median | wall p95 | wall max | daemon median | elements | notes |');
4042
lines.push('|---|---|---|---|---|---|---|---|---|---|---|');

0 commit comments

Comments
 (0)