Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/core/platform-plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ import type { CapabilityBucket } from '../platform-descriptor/types.ts';
* parity test before a real call-site routes through it. A facet's type stays
* PLATFORM-NEUTRAL and daemon-owned (never the iOS-simulator-shaped provider seam):
* {@link PlatformPlugin.appLog} carries the neutral {@link LogBackend} resolver
* (wraps `resolveLogBackend`, pinned by the daemon app-log routing parity test).
* The remaining columns (`providers` / `recording` / `perf`) stay on their daemon
* branch as the source of truth until they clear the same gate. See
* (wraps `resolveLogBackend`, pinned by the daemon app-log routing parity test);
* {@link PlatformPlugin.perf} carries the neutral perf-metrics support predicate
* (wraps `supportsPlatformPerfMetrics`, pinned by the daemon perf routing parity
* test). The remaining columns (`providers` / `recording`) — and the rest of the
* `perf` facet (the sampling body `buildPerfResponseData` and the Android-only
* native-collector gate) — stay on their daemon branch as the source of truth
* until each clears the same gate. See
* docs/adr/0009-apple-platform-consolidation.md (tracked in issue #974).
*/
export type PlatformPlugin = {
Expand Down Expand Up @@ -74,6 +78,21 @@ export type PlatformPlugin = {
readonly appLog?: {
resolveBackend(device: DeviceInfo): LogBackend;
};
/**
* The daemon perf facet (issue #974). `supportsMetrics` wraps the platform
* predicate `supportsPlatformPerfMetrics` in
* `src/daemon/handlers/session-perf.ts`, reporting whether `device`'s platform
* can produce session perf metrics (startup/fps/memory/cpu). Present only on
* families that expose perf metrics (Apple + Android); left `undefined` for
* linux/web, where the hand predicate returned `false` — the daemon lookup
* preserves that fallthrough, and the daemon perf routing parity test pins the
* equivalence. Only the support gate is routed today; the perf sampling body
* (`buildPerfResponseData`) and the Android-only native-collector gate stay on
* their daemon branch until each clears the same gate.
*/
readonly perf?: {
supportsMetrics(device: DeviceInfo): boolean;
};
};

// The single registry instance: leaf platform -> owning plugin. A family plugin
Expand Down
6 changes: 6 additions & 0 deletions src/core/platform-plugin/register-builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const applePlugin = {
? 'ios-device'
: 'ios-simulator',
},
// Wraps the Apple arm of `supportsPlatformPerfMetrics`: every Apple device
// (ios/macos, any kind/target) reports perf-metrics support.
perf: { supportsMetrics: () => true },
createInteractor: async (device: DeviceInfo, runner: RunnerContext) => {
const { createAppleInteractor } = await import('../interactors/apple.ts');
return createAppleInteractor(device, runner);
Expand All @@ -124,6 +127,9 @@ const androidPlugin = {
capability: { bucket: 'android' },
// Wraps the Android arm of `resolveLogBackend`: every Android device -> 'android'.
appLog: { resolveBackend: () => 'android' },
// Wraps the Android arm of `supportsPlatformPerfMetrics`: every Android device
// reports perf-metrics support.
perf: { supportsMetrics: () => true },
createInteractor: async (device: DeviceInfo) => {
const { createAndroidInteractor } = await import('../interactors/android.ts');
return createAndroidInteractor(device);
Expand Down
129 changes: 129 additions & 0 deletions src/daemon/__tests__/perf-plugin-routing-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import {
DEVICE_TARGETS,
PLATFORMS,
type DeviceInfo,
type DeviceKind,
type DeviceTarget,
} from '../../kernel/device.ts';
import {
ANDROID_EMULATOR,
ANDROID_TV_DEVICE,
IOS_DEVICE,
IOS_SIMULATOR,
LINUX_DEVICE,
MACOS_DEVICE,
makeSession,
TVOS_SIMULATOR,
WEB_DESKTOP_DEVICE,
} from '../../__tests__/test-utils/index.ts';
import { getPlugin } from '../../core/platform-plugin/plugin.ts';
import { registerBuiltinPlatformPlugins } from '../../core/platform-plugin/register-builtins.ts';
import { buildPerfResponseData } from '../handlers/session-perf.ts';
import { PERF_UNAVAILABLE_REASON } from '../handlers/session-startup-metrics.ts';

// Phase 3 step b.3 (issue #974) parity gate for the daemon perf facet. The
// per-platform gate of `supportsPlatformPerfMetrics` now flows through the
// PlatformPlugin `perf.supportsMetrics` facet instead of a hand disjunction. An
// INDEPENDENT verbatim copy of the former predicate below is the BEFORE oracle: a
// plugin-vs-branch disagreement on any sample device fails this test. (Mirrors the
// verbatim-copy discipline in daemon/__tests__/applog-plugin-routing-parity.test.ts
// and core/__tests__/capability-plugin-routing-parity.test.ts.)

registerBuiltinPlatformPlugins();

// --- INDEPENDENT verbatim copy of the former `supportsPlatformPerfMetrics` branch ---
function supportsPlatformPerfMetricsByHand(device: DeviceInfo): boolean {
return device.platform === 'android' || device.platform === 'ios' || device.platform === 'macos';
}

// --- the exhaustive synthetic device matrix (every platform x kind x target) ---
const DEVICE_KINDS_ALL: DeviceKind[] = ['simulator', 'emulator', 'device'];
const DEVICE_TARGETS_ALL: (DeviceTarget | undefined)[] = [undefined, ...DEVICE_TARGETS];

function buildDeviceMatrix(): DeviceInfo[] {
const devices: DeviceInfo[] = [];
for (const platform of PLATFORMS) {
for (const kind of DEVICE_KINDS_ALL) {
for (const target of DEVICE_TARGETS_ALL) {
devices.push({
platform,
id: `${platform}-${kind}-${target ?? 'none'}`,
name: `${platform} ${kind} ${target ?? 'none'}`,
kind,
...(target ? { target } : {}),
booted: true,
});
}
}
}
return devices;
}

// The hand-authored fixtures (the real discovery shapes) plus the exhaustive
// synthetic cross-product, so every off-nominal combination is pinned too.
const SAMPLE_DEVICES: DeviceInfo[] = [
ANDROID_EMULATOR,
ANDROID_TV_DEVICE,
IOS_DEVICE,
IOS_SIMULATOR,
LINUX_DEVICE,
MACOS_DEVICE,
TVOS_SIMULATOR,
WEB_DESKTOP_DEVICE,
...buildDeviceMatrix(),
];

test('perf.supportsMetrics facet is byte-identical to the former hand predicate', () => {
for (const device of SAMPLE_DEVICES) {
assert.equal(
getPlugin(device.platform).perf?.supportsMetrics(device) ?? false,
supportsPlatformPerfMetricsByHand(device),
`supportsMetrics for ${device.id}`,
);
}
});

test('only families with perf metrics carry the perf facet', () => {
// Apple owns ios + macos (SAME plugin instance); Android carries its own.
assert.equal(getPlugin('ios'), getPlugin('macos'));
assert.ok(getPlugin('ios').perf, 'apple plugin exposes perf');
assert.ok(getPlugin('android').perf, 'android plugin exposes perf');
// linux/web historically returned `false`; they get NO facet, and the daemon
// lookup preserves that fallthrough (asserted below).
assert.equal(getPlugin('linux').perf, undefined, 'linux plugin has no perf');
assert.equal(getPlugin('web').perf, undefined, 'web plugin has no perf');
});

test('the factless families fall through to the historical `false` default', () => {
for (const device of SAMPLE_DEVICES.filter(
(d) => d.platform === 'linux' || d.platform === 'web',
)) {
assert.equal(getPlugin(device.platform).perf, undefined);
assert.equal(
getPlugin(device.platform).perf?.supportsMetrics(device) ?? false,
false,
`fallthrough for ${device.id}`,
);
}
});

// End-to-end routing proof: `buildPerfResponseData` consults the perf facet via the
// (private) `supportsPlatformPerfMetrics`. For a session with NO app bundle it does
// no device I/O — an UNSUPPORTED platform returns the default-unavailable base
// response (cpu reason `PERF_UNAVAILABLE_REASON`), while a SUPPORTED platform fills
// the missing-app reason instead. So the routed cpu reason discriminates support,
// and breaking the facet would flip the classification and fail this test.
test('buildPerfResponseData routes the support gate through the perf facet', async () => {
for (const device of SAMPLE_DEVICES) {
const data = await buildPerfResponseData(makeSession(`perf-${device.id}`, { device }));
const cpu = data.metrics.cpu as { reason?: string };
const routedSupportsMetrics = cpu.reason !== PERF_UNAVAILABLE_REASON;
assert.equal(
routedSupportsMetrics,
supportsPlatformPerfMetricsByHand(device),
`routed support for ${device.id}`,
);
}
});
18 changes: 13 additions & 5 deletions src/daemon/handlers/session-perf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import path from 'node:path';
import type { SessionAction, SessionState } from '../types.ts';
import { AppError, normalizeError } from '../../kernel/errors.ts';
import { isApplePlatform } from '../../kernel/device.ts';
import { tryGetPlugin } from '../../core/platform-plugin/plugin.ts';
import { registerBuiltinPlatformPlugins } from '../../core/platform-plugin/register-builtins.ts';
import type { AndroidAdbExecutor } from '../../platforms/android/adb-executor.ts';
import {
ANDROID_HPROF_SNAPSHOT_DESCRIPTION,
Expand Down Expand Up @@ -37,6 +39,12 @@ import {
type StartupPerfSample,
} from './session-startup-metrics.ts';

// Populate the PlatformPlugin registry once at module load (idempotent; registers
// only lazy closures, so no leaf code is imported and CLI cold-start is unaffected
// — mirrors the same call in `daemon/app-log.ts`). `supportsPlatformPerfMetrics`
// reads the per-platform perf facet from this registry, so it must be populated first.
registerBuiltinPlatformPlugins();

type SettledMetricResult = PromiseSettledResult<Record<string, unknown>>;
type MetricResult =
| ({ available: true } & Record<string, unknown>)
Expand Down Expand Up @@ -322,11 +330,11 @@ async function applyFramePerfMetric(
}

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

function buildMissingAppPerfReason(session: SessionState): string {
Expand Down
Loading