Skip to content

Commit 6e8e45f

Browse files
authored
refactor(daemon): route perf support gate through PlatformPlugin perf facet (#995)
Add the `perf` facet to `PlatformPlugin`, typed platform-neutral as `{ supportsMetrics(device: DeviceInfo): boolean }` (never the iOS provider seam). Populate it by wrapping the Apple + Android arms of the existing `supportsPlatformPerfMetrics` predicate verbatim (both return `true`), leave linux/web factless, and route `supportsPlatformPerfMetrics` in `daemon/handlers/session-perf.ts` through `tryGetPlugin(...).perf?.supportsMetrics` with a `?? false` fallthrough that preserves the former hand disjunction. Pinned by a table-equivalence parity test (`daemon/__tests__/perf-plugin-routing-parity.test.ts`) that mirrors the merged appLog gate: an independent verbatim copy of the former predicate is the BEFORE oracle across the exhaustive platform x kind x target device matrix, facet presence/fallthrough are asserted, and `buildPerfResponseData` is exercised with no-app sessions to prove the daemon actually routes through the facet. Only the support gate is routed; the perf sampling body (`buildPerfResponseData`) and the Android-only native-collector gate stay on their daemon branch until each clears the same gate. Follows the merged appLog facet template (type-only core->daemon edge, lazy `registerBuiltinPlatformPlugins()`). Refs #974
1 parent 56b41a5 commit 6e8e45f

4 files changed

Lines changed: 170 additions & 8 deletions

File tree

‎src/core/platform-plugin/plugin.ts‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ import type { CapabilityBucket } from '../platform-descriptor/types.ts';
2424
* parity test before a real call-site routes through it. A facet's type stays
2525
* PLATFORM-NEUTRAL and daemon-owned (never the iOS-simulator-shaped provider seam):
2626
* {@link PlatformPlugin.appLog} carries the neutral {@link LogBackend} resolver
27-
* (wraps `resolveLogBackend`, pinned by the daemon app-log routing parity test).
28-
* The remaining columns (`providers` / `recording` / `perf`) stay on their daemon
29-
* branch as the source of truth until they clear the same gate. See
27+
* (wraps `resolveLogBackend`, pinned by the daemon app-log routing parity test);
28+
* {@link PlatformPlugin.perf} carries the neutral perf-metrics support predicate
29+
* (wraps `supportsPlatformPerfMetrics`, pinned by the daemon perf routing parity
30+
* test). The remaining columns (`providers` / `recording`) — and the rest of the
31+
* `perf` facet (the sampling body `buildPerfResponseData` and the Android-only
32+
* native-collector gate) — stay on their daemon branch as the source of truth
33+
* until each clears the same gate. See
3034
* docs/adr/0009-apple-platform-consolidation.md (tracked in issue #974).
3135
*/
3236
export type PlatformPlugin = {
@@ -74,6 +78,21 @@ export type PlatformPlugin = {
7478
readonly appLog?: {
7579
resolveBackend(device: DeviceInfo): LogBackend;
7680
};
81+
/**
82+
* The daemon perf facet (issue #974). `supportsMetrics` wraps the platform
83+
* predicate `supportsPlatformPerfMetrics` in
84+
* `src/daemon/handlers/session-perf.ts`, reporting whether `device`'s platform
85+
* can produce session perf metrics (startup/fps/memory/cpu). Present only on
86+
* families that expose perf metrics (Apple + Android); left `undefined` for
87+
* linux/web, where the hand predicate returned `false` — the daemon lookup
88+
* preserves that fallthrough, and the daemon perf routing parity test pins the
89+
* equivalence. Only the support gate is routed today; the perf sampling body
90+
* (`buildPerfResponseData`) and the Android-only native-collector gate stay on
91+
* their daemon branch until each clears the same gate.
92+
*/
93+
readonly perf?: {
94+
supportsMetrics(device: DeviceInfo): boolean;
95+
};
7796
};
7897

7998
// The single registry instance: leaf platform -> owning plugin. A family plugin

‎src/core/platform-plugin/register-builtins.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ const applePlugin = {
100100
? 'ios-device'
101101
: 'ios-simulator',
102102
},
103+
// Wraps the Apple arm of `supportsPlatformPerfMetrics`: every Apple device
104+
// (ios/macos, any kind/target) reports perf-metrics support.
105+
perf: { supportsMetrics: () => true },
103106
createInteractor: async (device: DeviceInfo, runner: RunnerContext) => {
104107
const { createAppleInteractor } = await import('../interactors/apple.ts');
105108
return createAppleInteractor(device, runner);
@@ -128,6 +131,9 @@ const androidPlugin = {
128131
},
129132
// Wraps the Android arm of `resolveLogBackend`: every Android device -> 'android'.
130133
appLog: { resolveBackend: () => 'android' },
134+
// Wraps the Android arm of `supportsPlatformPerfMetrics`: every Android device
135+
// reports perf-metrics support.
136+
perf: { supportsMetrics: () => true },
131137
createInteractor: async (device: DeviceInfo) => {
132138
const { createAndroidInteractor } = await import('../interactors/android.ts');
133139
return createAndroidInteractor(device);
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import {
4+
DEVICE_TARGETS,
5+
PLATFORMS,
6+
type DeviceInfo,
7+
type DeviceKind,
8+
type DeviceTarget,
9+
} from '../../kernel/device.ts';
10+
import {
11+
ANDROID_EMULATOR,
12+
ANDROID_TV_DEVICE,
13+
IOS_DEVICE,
14+
IOS_SIMULATOR,
15+
LINUX_DEVICE,
16+
MACOS_DEVICE,
17+
makeSession,
18+
TVOS_SIMULATOR,
19+
WEB_DESKTOP_DEVICE,
20+
} from '../../__tests__/test-utils/index.ts';
21+
import { getPlugin } from '../../core/platform-plugin/plugin.ts';
22+
import { registerBuiltinPlatformPlugins } from '../../core/platform-plugin/register-builtins.ts';
23+
import { buildPerfResponseData } from '../handlers/session-perf.ts';
24+
import { PERF_UNAVAILABLE_REASON } from '../handlers/session-startup-metrics.ts';
25+
26+
// Phase 3 step b.3 (issue #974) parity gate for the daemon perf facet. The
27+
// per-platform gate of `supportsPlatformPerfMetrics` now flows through the
28+
// PlatformPlugin `perf.supportsMetrics` facet instead of a hand disjunction. An
29+
// INDEPENDENT verbatim copy of the former predicate below is the BEFORE oracle: a
30+
// plugin-vs-branch disagreement on any sample device fails this test. (Mirrors the
31+
// verbatim-copy discipline in daemon/__tests__/applog-plugin-routing-parity.test.ts
32+
// and core/__tests__/capability-plugin-routing-parity.test.ts.)
33+
34+
registerBuiltinPlatformPlugins();
35+
36+
// --- INDEPENDENT verbatim copy of the former `supportsPlatformPerfMetrics` branch ---
37+
function supportsPlatformPerfMetricsByHand(device: DeviceInfo): boolean {
38+
return device.platform === 'android' || device.platform === 'ios' || device.platform === 'macos';
39+
}
40+
41+
// --- the exhaustive synthetic device matrix (every platform x kind x target) ---
42+
const DEVICE_KINDS_ALL: DeviceKind[] = ['simulator', 'emulator', 'device'];
43+
const DEVICE_TARGETS_ALL: (DeviceTarget | undefined)[] = [undefined, ...DEVICE_TARGETS];
44+
45+
function buildDeviceMatrix(): DeviceInfo[] {
46+
const devices: DeviceInfo[] = [];
47+
for (const platform of PLATFORMS) {
48+
for (const kind of DEVICE_KINDS_ALL) {
49+
for (const target of DEVICE_TARGETS_ALL) {
50+
devices.push({
51+
platform,
52+
id: `${platform}-${kind}-${target ?? 'none'}`,
53+
name: `${platform} ${kind} ${target ?? 'none'}`,
54+
kind,
55+
...(target ? { target } : {}),
56+
booted: true,
57+
});
58+
}
59+
}
60+
}
61+
return devices;
62+
}
63+
64+
// The hand-authored fixtures (the real discovery shapes) plus the exhaustive
65+
// synthetic cross-product, so every off-nominal combination is pinned too.
66+
const SAMPLE_DEVICES: DeviceInfo[] = [
67+
ANDROID_EMULATOR,
68+
ANDROID_TV_DEVICE,
69+
IOS_DEVICE,
70+
IOS_SIMULATOR,
71+
LINUX_DEVICE,
72+
MACOS_DEVICE,
73+
TVOS_SIMULATOR,
74+
WEB_DESKTOP_DEVICE,
75+
...buildDeviceMatrix(),
76+
];
77+
78+
test('perf.supportsMetrics facet is byte-identical to the former hand predicate', () => {
79+
for (const device of SAMPLE_DEVICES) {
80+
assert.equal(
81+
getPlugin(device.platform).perf?.supportsMetrics(device) ?? false,
82+
supportsPlatformPerfMetricsByHand(device),
83+
`supportsMetrics for ${device.id}`,
84+
);
85+
}
86+
});
87+
88+
test('only families with perf metrics carry the perf facet', () => {
89+
// Apple owns ios + macos (SAME plugin instance); Android carries its own.
90+
assert.equal(getPlugin('ios'), getPlugin('macos'));
91+
assert.ok(getPlugin('ios').perf, 'apple plugin exposes perf');
92+
assert.ok(getPlugin('android').perf, 'android plugin exposes perf');
93+
// linux/web historically returned `false`; they get NO facet, and the daemon
94+
// lookup preserves that fallthrough (asserted below).
95+
assert.equal(getPlugin('linux').perf, undefined, 'linux plugin has no perf');
96+
assert.equal(getPlugin('web').perf, undefined, 'web plugin has no perf');
97+
});
98+
99+
test('the factless families fall through to the historical `false` default', () => {
100+
for (const device of SAMPLE_DEVICES.filter(
101+
(d) => d.platform === 'linux' || d.platform === 'web',
102+
)) {
103+
assert.equal(getPlugin(device.platform).perf, undefined);
104+
assert.equal(
105+
getPlugin(device.platform).perf?.supportsMetrics(device) ?? false,
106+
false,
107+
`fallthrough for ${device.id}`,
108+
);
109+
}
110+
});
111+
112+
// End-to-end routing proof: `buildPerfResponseData` consults the perf facet via the
113+
// (private) `supportsPlatformPerfMetrics`. For a session with NO app bundle it does
114+
// no device I/O — an UNSUPPORTED platform returns the default-unavailable base
115+
// response (cpu reason `PERF_UNAVAILABLE_REASON`), while a SUPPORTED platform fills
116+
// the missing-app reason instead. So the routed cpu reason discriminates support,
117+
// and breaking the facet would flip the classification and fail this test.
118+
test('buildPerfResponseData routes the support gate through the perf facet', async () => {
119+
for (const device of SAMPLE_DEVICES) {
120+
const data = await buildPerfResponseData(makeSession(`perf-${device.id}`, { device }));
121+
const cpu = data.metrics.cpu as { reason?: string };
122+
const routedSupportsMetrics = cpu.reason !== PERF_UNAVAILABLE_REASON;
123+
assert.equal(
124+
routedSupportsMetrics,
125+
supportsPlatformPerfMetricsByHand(device),
126+
`routed support for ${device.id}`,
127+
);
128+
}
129+
});

‎src/daemon/handlers/session-perf.ts‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import path from 'node:path';
22
import type { SessionAction, SessionState } from '../types.ts';
33
import { AppError, normalizeError } from '../../kernel/errors.ts';
44
import { isApplePlatform } from '../../kernel/device.ts';
5+
import { tryGetPlugin } from '../../core/platform-plugin/plugin.ts';
6+
import { registerBuiltinPlatformPlugins } from '../../core/platform-plugin/register-builtins.ts';
57
import type { AndroidAdbExecutor } from '../../platforms/android/adb-executor.ts';
68
import {
79
ANDROID_HPROF_SNAPSHOT_DESCRIPTION,
@@ -37,6 +39,12 @@ import {
3739
type StartupPerfSample,
3840
} from './session-startup-metrics.ts';
3941

42+
// Populate the PlatformPlugin registry once at module load (idempotent; registers
43+
// only lazy closures, so no leaf code is imported and CLI cold-start is unaffected
44+
// — mirrors the same call in `daemon/app-log.ts`). `supportsPlatformPerfMetrics`
45+
// reads the per-platform perf facet from this registry, so it must be populated first.
46+
registerBuiltinPlatformPlugins();
47+
4048
type SettledMetricResult = PromiseSettledResult<Record<string, unknown>>;
4149
type MetricResult =
4250
| ({ available: true } & Record<string, unknown>)
@@ -322,11 +330,11 @@ async function applyFramePerfMetric(
322330
}
323331

324332
function supportsPlatformPerfMetrics(session: SessionState): boolean {
325-
return (
326-
session.device.platform === 'android' ||
327-
session.device.platform === 'ios' ||
328-
session.device.platform === 'macos'
329-
);
333+
// Routes the platform gate through the PlatformPlugin perf facet (issue #974).
334+
// Apple/Android carry `perf.supportsMetrics`; linux/web (and any unregistered
335+
// platform) fall through to `false`, matching the former hand predicate. The
336+
// daemon perf routing parity test pins this equivalence.
337+
return tryGetPlugin(session.device.platform)?.perf?.supportsMetrics(session.device) ?? false;
330338
}
331339

332340
function buildMissingAppPerfReason(session: SessionState): string {

0 commit comments

Comments
 (0)