mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
b15121ffc8
* test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * style(size): format package evidence test * test(size): update publish preparation contracts * fix: retire stale utils layering zone * test: pin shared publish asset owner * test: verify preserved size reporter closure * fix: move mutation ownership to snapshot module * test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * test(size): update publish preparation contracts * fix: keep git-state gates out of mutation sandboxes
285 lines
8.9 KiB
TypeScript
285 lines
8.9 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
BenchmarkConfigurationError,
|
|
parseConfig,
|
|
type BenchmarkConfig,
|
|
} from './benchmark-config.ts';
|
|
import { classifyFailure, formatCliFailure, openFixture, type CliContext } from './command.ts';
|
|
import { BenchmarkControlError, runDeepButtonControls } from './deep-control.ts';
|
|
import { CONTRACT, screenFixture } from './definitions.ts';
|
|
import { deepButtonFixtureEvidence } from './deep-button.ts';
|
|
import { readGitRevision, readHostIdentity, readTarget, readToolchain } from './host.ts';
|
|
import {
|
|
BenchmarkCellAdmissionError,
|
|
BenchmarkContentionError,
|
|
BenchmarkInfrastructureError,
|
|
bootSimulator,
|
|
shutdownSimulator,
|
|
stopDaemon,
|
|
} from './lifecycle.ts';
|
|
import { closeSession, runLocalMeasurements } from './local-runner.ts';
|
|
import { measurePackageSize, notRunPackageSize } from './package-evidence.ts';
|
|
import { runProxyMeasurements } from './proxy-runner.ts';
|
|
import { renderBenchmarkMarkdown } from './report.ts';
|
|
import { assertValidRawResult } from './schema.ts';
|
|
import {
|
|
BENCHMARK_SCHEMA_VERSION,
|
|
type BenchmarkStop,
|
|
type BenchmarkResult,
|
|
type DeepButtonEvidence,
|
|
type GitRevision,
|
|
type HostIdentity,
|
|
type Measurement,
|
|
type PackageSize,
|
|
type ProxyNetwork,
|
|
type Target,
|
|
type Toolchain,
|
|
} from './types.ts';
|
|
|
|
type BenchmarkMetadata = {
|
|
revision: GitRevision;
|
|
target: Target;
|
|
toolchain: Toolchain;
|
|
host: HostIdentity;
|
|
};
|
|
type BenchmarkEvidence = {
|
|
packageSize: PackageSize;
|
|
deepButtonEvidence: DeepButtonEvidence;
|
|
measurements: Measurement[];
|
|
proxyNetworks?: ProxyNetwork[];
|
|
};
|
|
|
|
if (isMainModule()) {
|
|
try {
|
|
await main(process.argv.slice(2));
|
|
} catch (error) {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
async function main(argv: string[]): Promise<void> {
|
|
const config = parseConfig(argv);
|
|
const metadata = readMetadata(config);
|
|
const result = await executeBenchmark(config, metadata);
|
|
assertValidRawResult(result);
|
|
writeResult(config.outputPath, result);
|
|
process.stdout.write(`${renderBenchmarkMarkdown(result)}\nraw: ${config.outputPath}\n`);
|
|
if (result.status === 'stopped') process.exitCode = 2;
|
|
}
|
|
|
|
function readMetadata(config: BenchmarkConfig): BenchmarkMetadata {
|
|
return {
|
|
revision: readGitRevision(config.repoRoot),
|
|
target: readTarget(config.udid, config.appId, config.appPath),
|
|
toolchain: readToolchain(),
|
|
host: readHostIdentity(),
|
|
};
|
|
}
|
|
|
|
async function executeBenchmark(
|
|
config: BenchmarkConfig,
|
|
metadata: BenchmarkMetadata,
|
|
): Promise<BenchmarkResult> {
|
|
const evidence: BenchmarkEvidence = {
|
|
packageSize: notRunPackageSize(metadata.revision.commit),
|
|
deepButtonEvidence: deepButtonFixtureEvidence(),
|
|
measurements: [],
|
|
};
|
|
try {
|
|
bootSimulator(config.udid);
|
|
primeDeepLink(config);
|
|
evidence.deepButtonEvidence = runDeepButtonControls(config.repoRoot);
|
|
if (!config.skipPackageSize) {
|
|
evidence.packageSize = measurePackageSize(config.repoRoot, metadata.revision.commit);
|
|
}
|
|
const measurements = await collectMeasurements(config);
|
|
evidence.measurements = measurements.measurements;
|
|
evidence.proxyNetworks = measurements.proxyNetworks;
|
|
return completedResult(config, metadata, evidence);
|
|
} catch (error) {
|
|
return stoppedResult(config, metadata, evidence, error);
|
|
} finally {
|
|
shutdownAfterRun(config);
|
|
}
|
|
}
|
|
|
|
function primeDeepLink(config: BenchmarkConfig): void {
|
|
const fixture = config.screens.map(screenFixture).find((candidate) => candidate.launchUrl);
|
|
if (!fixture) return;
|
|
const context: CliContext = {
|
|
repoRoot: config.repoRoot,
|
|
stateDir: config.stateDir,
|
|
session: 'bench-deep-link-prime',
|
|
udid: config.udid,
|
|
derivedPath: path.join(config.derivedPath, 'deep-link-prime'),
|
|
};
|
|
try {
|
|
const opened = openFixture(context, fixture, { relaunch: true });
|
|
if (!opened.ok) {
|
|
throw new BenchmarkInfrastructureError(
|
|
formatCliFailure('deep-link priming', classifyFailure(opened.payload, opened), opened),
|
|
'agent-device open --launch-url',
|
|
);
|
|
}
|
|
} finally {
|
|
closeSession(context);
|
|
stopDaemon(config.repoRoot, config.stateDir);
|
|
}
|
|
}
|
|
|
|
async function collectMeasurements(config: BenchmarkConfig): Promise<{
|
|
measurements: Measurement[];
|
|
proxyNetworks?: ProxyNetwork[];
|
|
}> {
|
|
const measurements = shouldRunLocal(config)
|
|
? await runLocalMeasurements({
|
|
repoRoot: config.repoRoot,
|
|
stateDir: config.stateDir,
|
|
derivedPath: config.derivedPath,
|
|
udid: config.udid,
|
|
fixtures: config.screens.map(screenFixture),
|
|
states: config.states,
|
|
samples: config.samples,
|
|
})
|
|
: [];
|
|
if (!shouldRunProxy(config)) return { measurements };
|
|
const proxy = await runProxyMeasurements({
|
|
repoRoot: config.repoRoot,
|
|
stateDir: path.join(config.stateDir, 'proxy'),
|
|
clientStateDir: path.join(config.stateDir, 'proxy-clients'),
|
|
derivedPath: config.derivedPath,
|
|
udid: config.udid,
|
|
fixtures: config.screens.map(screenFixture),
|
|
samples: config.samples,
|
|
rtts: config.rtts,
|
|
bandwidthKbps: config.bandwidthKbps,
|
|
packetLossPercent: config.packetLossPercent,
|
|
seed: config.seed,
|
|
});
|
|
return { measurements: [...measurements, ...proxy.measurements], proxyNetworks: proxy.networks };
|
|
}
|
|
|
|
function shouldRunLocal(config: BenchmarkConfig): boolean {
|
|
return config.mode === 'local' || config.mode === 'all';
|
|
}
|
|
|
|
function shouldRunProxy(config: BenchmarkConfig): boolean {
|
|
return config.mode === 'proxy' || config.mode === 'all';
|
|
}
|
|
|
|
function completedResult(
|
|
config: BenchmarkConfig,
|
|
metadata: BenchmarkMetadata,
|
|
evidence: BenchmarkEvidence,
|
|
): BenchmarkResult {
|
|
return {
|
|
...resultFields(config, metadata, evidence),
|
|
status: 'completed',
|
|
measurements: evidence.measurements,
|
|
...(evidence.proxyNetworks ? { proxyNetworks: evidence.proxyNetworks } : {}),
|
|
};
|
|
}
|
|
|
|
function stoppedResult(
|
|
config: BenchmarkConfig,
|
|
metadata: BenchmarkMetadata,
|
|
evidence: BenchmarkEvidence,
|
|
error: unknown,
|
|
): BenchmarkResult {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
process.stderr.write(`[ios-benchmark] stopped: ${message}\n`);
|
|
return {
|
|
...resultFields(config, metadata, evidence),
|
|
status: 'stopped',
|
|
measurements: evidence.measurements,
|
|
...(evidence.proxyNetworks ? { proxyNetworks: evidence.proxyNetworks } : {}),
|
|
stop: {
|
|
category: stopCategory(error),
|
|
message: message.slice(0, 2000),
|
|
...stopDetails(error),
|
|
},
|
|
};
|
|
}
|
|
|
|
function stopDetails(error: unknown): Pick<BenchmarkStop, 'reason' | 'command'> {
|
|
const reason = error instanceof BenchmarkCellAdmissionError ? error.reason : undefined;
|
|
const command = stopCommand(error);
|
|
return {
|
|
...(reason ? { reason } : {}),
|
|
...(command ? { command } : {}),
|
|
};
|
|
}
|
|
|
|
function stopCategory(error: unknown): 'infrastructure' | 'contention' | 'configuration' {
|
|
if (error instanceof BenchmarkConfigurationError) return 'configuration';
|
|
if (error instanceof BenchmarkContentionError) return 'contention';
|
|
return 'infrastructure';
|
|
}
|
|
|
|
function stopCommand(error: unknown): string | undefined {
|
|
if (
|
|
error instanceof BenchmarkControlError ||
|
|
error instanceof BenchmarkInfrastructureError ||
|
|
error instanceof BenchmarkContentionError
|
|
) {
|
|
return error.command;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function shutdownAfterRun(config: BenchmarkConfig): void {
|
|
if (config.keepDevice) return;
|
|
try {
|
|
shutdownSimulator(config.udid);
|
|
} catch {
|
|
process.stderr.write(`[ios-benchmark] cleanup could not shut down ${config.udid}\n`);
|
|
}
|
|
}
|
|
|
|
function resultFields(
|
|
config: BenchmarkConfig,
|
|
metadata: BenchmarkMetadata,
|
|
evidence: BenchmarkEvidence,
|
|
) {
|
|
return {
|
|
schemaVersion: BENCHMARK_SCHEMA_VERSION,
|
|
issue: CONTRACT.issue,
|
|
parent: CONTRACT.parent,
|
|
references: CONTRACT.references,
|
|
runId: `ios-snapshot-${Date.now()}-${process.pid}`,
|
|
generatedAt: new Date().toISOString(),
|
|
revision: metadata.revision,
|
|
toolchain: metadata.toolchain,
|
|
host: metadata.host,
|
|
target: metadata.target,
|
|
config: {
|
|
warmSampleMinimum: CONTRACT.warmSampleMinimum,
|
|
coldSampleMinimum: CONTRACT.coldSampleMinimum,
|
|
requestedSamples: config.samples,
|
|
screens: config.screens,
|
|
states: config.states,
|
|
},
|
|
measurements: [],
|
|
packageSize: evidence.packageSize,
|
|
deepButtonEvidence: evidence.deepButtonEvidence,
|
|
} satisfies Omit<BenchmarkResult, 'status' | 'stop' | 'proxyNetworks'>;
|
|
}
|
|
|
|
function writeResult(outputPath: string, result: BenchmarkResult): void {
|
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
fs.writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
fs.writeFileSync(outputPath.replace(/\.json$/u, '.md'), renderBenchmarkMarkdown(result));
|
|
}
|
|
|
|
function isMainModule(): boolean {
|
|
return Boolean(
|
|
process.argv[1] &&
|
|
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url)),
|
|
);
|
|
}
|