Files
iptvnator/apps/electron-backend-e2e/src/performance/xtream-exact-schema.ts
T
4grayandClaude Opus 5.5 078b91bfdf test(perf): type-check the whole electron-backend-e2e project (#1869)
* test(e2e): fix type errors in four Electron E2E specs

- category-management: the poll's message and 15 s timeout were passed to
  toBe(), which takes one argument, so the poll ran with the default 10 s
  timeout and message. Pass them to expect.poll().
- settings: evaluate<void, HTMLVideoElement> set the argument type, not the
  element type; type the callback parameter instead.
- remote-control: keep the poll callback's writes visible to the return and
  fail with a clear error instead of casting null.
- downloads: read the dataset key through its index signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): fix type errors in the benchmark harness

None of these changes behaviour for a capture the harness produces today:

- cancellation report: a null playlist id on the request record now means
  unknown, like undefined, instead of matching only null DB records.
- main capture: type the worker transport map so its unavailable-reason
  literals stay narrow.
- worker request performance: a parsed capture carries non-null work
  timestamps; only the unavailable fallback reports null.
- Xtream iteration: hand the teardown closure the narrowed app and data
  directory.
- Xtream exact schema: return early when the identity is not an exact
  record instead of re-checking it three times.
- Xtream assembly: reject an unpainted UI explicitly; the null-to-0
  comparison already rejected it.
- Xtream scenario driver: drop the always-null cancellation observer from
  prepare; trigger() installs it.
- Electron process lifecycle: type the child process by the exit
  subscription it uses, so a test double need not return a ChildProcess.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): fix type errors in the benchmark harness specs

The dynamically imported helpers were checked with
assert.equal(typeof fn, 'function'), which does not narrow, so later calls
used fn?.(). assert.ok(typeof fn === 'function') narrows them. The rest
types fixtures by the module under test, keeps closure-assigned callbacks
visible after their callbacks run, and narrows nullable fixture values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): type-check the whole electron-backend-e2e project

Widen the spec config's include from the journey harness to src/**/*.ts
and the root Playwright configs, with no baseline or ignore list, and
update the validation map and the journey checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(perf): build a complete renderer fixture in the request-performance spec

The fixture was cast through unknown, so the type check accepted an
incomplete RendererCaptureMetrics. Fill every required field with neutral
values; the summary reads only the probe arrays and heap fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:33:15 +02:00

389 lines
11 KiB
TypeScript

import {
XTREAM_ITERATION_KIND,
type XtreamIterationDefinition,
} from './xtream-benchmark-contract';
import {
XTREAM_IDENTIFIER,
XTREAM_SHA256,
} from './xtream-benchmark-manifest-schema';
import {
XTREAM_NOT_APPLICABLE,
XTREAM_STARTUP_RETRY_REASON,
type XtreamRawIterationResult,
type XtreamValidatedIterationResult,
} from './xtream-summary-contract';
export {
assertXtreamBenchmarkManifest,
parseXtreamBenchmarkManifest,
XTREAM_IDENTIFIER,
XTREAM_SHA256,
} from './xtream-benchmark-manifest-schema';
export const XTREAM_RAW_ITERATION_KEYS = [
'cancellation',
'cancellationEvidence',
'catalogVerification',
'databaseWorker',
'eligibleForComparison',
'kind',
'main',
'phaseCapture',
'phases',
'playlistRefreshWorker',
'processIdentity',
'renderer',
'runId',
'scenarioId',
'validity',
] as const;
export const XTREAM_PHASE_NUMBER_KEYS = [
'angularRenderingMs',
'dataAcquisitionMs',
'deserializationMs',
'jsonParsingMs',
'normalizationMs',
'serializationMs',
'sqliteReadMs',
'sqliteWriteMs',
'storeUpdateMs',
'structuredCloneProxyMs',
'totalMs',
] as const;
export const XTREAM_PHASE_KEYS = [
...XTREAM_PHASE_NUMBER_KEYS,
'backgroundSearchMarkerMs',
'backgroundSearchMarkerReason',
'indexesMs',
'indexesReason',
'restoreUserDataMs',
'restoreUserDataReason',
'structuredCloneAttribution',
'transactionCommitMs',
'transactionCommitReason',
] as const;
export const XTREAM_PROFILE_KEYS = [
'cpuProfileDurationMicros',
'cpuProfileUnavailableReason',
] as const;
export const XTREAM_RENDERER_NUMBER_KEYS = [
'frameGapMaxMs',
'frameGapP95Ms',
'heartbeatMaxMs',
'heartbeatP95Ms',
'longTaskCount',
'longTaskMaxMs',
'peakHeapUsedBytes',
'peakRssBytes',
'postGcHeapUsedBytes',
'storeToPaintMs',
] as const;
export const XTREAM_MAIN_NUMBER_KEYS = [
'browserWindowId',
'cpuSystemMicros',
'cpuUserMicros',
'eventLoopDelayMaxMs',
'eventLoopDelayP95Ms',
'eventLoopDelayP99Ms',
'peakHeapUsedBytes',
'peakRssBytes',
'postGcHeapUsedBytes',
'responsiveEvents',
'unresponsiveEvents',
'webContentsId',
] as const;
export const XTREAM_WORKER_NUMBER_KEYS = [
'cpuSystemMicros',
'cpuUserMicros',
'eventLoopDelayMaxMs',
'eventLoopDelayP95Ms',
'eventLoopDelayP99Ms',
'eventLoopUtilization',
'ordinal',
'peakExternalBytes',
'peakHeapUsedBytes',
'postGcHeapUsedBytes',
] as const;
export const XTREAM_CANCEL_TIME_KEYS = [
'acknowledgementLatencyMs',
'authoritativeTerminalEpochMs',
'clickEpochMs',
'clickToDatabaseDispatchMs',
'clickToPaintMs',
'clickToPreloadReturnMs',
'databaseDispatchEpochMs',
'paintedEpochMs',
'preloadReturnEpochMs',
'preloadStartEpochMs',
'workerReceiptEpochMs',
'workerReceiptLatencyMs',
] as const;
export function assertFreshXtreamProcessIdentities(
iterations: readonly XtreamValidatedIterationResult[]
): void {
const unique = (select: (run: XtreamValidatedIterationResult) => unknown) =>
new Set(iterations.map(select)).size === iterations.length;
if (
!unique((run) => run.processIdentity.electronPid) ||
!unique((run) => run.processIdentity.launchId) ||
!unique((run) => run.processIdentity.profileDirectorySha256) ||
!unique((run) => run.processIdentity.generationIdentitySha256)
) {
throw new Error('Xtream benchmark requires a fresh process identity');
}
}
export function snapshotXtreamIterations(
value: unknown
): readonly XtreamValidatedIterationResult[] {
try {
if (!Array.isArray(value)) invalidIterationCollection();
const lengthDescriptor = Reflect.getOwnPropertyDescriptor(
value,
'length'
);
const length = lengthDescriptor?.value;
if (
!Number.isSafeInteger(length) ||
Number(length) < 0 ||
Number(length) > 128
) {
invalidIterationCollection();
}
const snapshot: XtreamValidatedIterationResult[] = [];
for (let index = 0; index < Number(length); index += 1) {
const descriptor = Reflect.getOwnPropertyDescriptor(
value,
String(index)
);
if (
!descriptor ||
!Object.prototype.hasOwnProperty.call(descriptor, 'value')
) {
invalidIterationCollection();
}
snapshot.push(descriptor.value as XtreamValidatedIterationResult);
}
return Object.freeze(snapshot);
} catch {
throw new Error('invalid Xtream iteration collection');
}
}
export function assertOrderedXtreamSchedule(
iterations: readonly XtreamValidatedIterationResult[],
expected: readonly XtreamIterationDefinition[],
mode: 'formal' | 'smoke'
): void {
if (iterations.length !== expected.length) scheduleError(mode);
for (let index = 0; index < expected.length; index += 1) {
const actual = iterations[index];
const definition = expected[index];
assertXtreamSafePathIdentifier(actual?.runId, 'run-id');
assertXtreamSafePathIdentifier(definition?.runId, 'run-id');
if (
!actual ||
!definition ||
actual.scenarioId !== definition.scenarioId ||
actual.runId !== definition.runId ||
actual.kind !== definition.kind ||
actual.eligibleForComparison !== definition.eligibleForComparison
) {
throw new Error('invalid ordered Xtream benchmark schedule');
}
}
}
export function assertXtreamSafePathIdentifier(
value: unknown,
label: string
): asserts value is string {
if (typeof value !== 'string' || !XTREAM_IDENTIFIER.test(value)) {
throw new Error(`benchmark requires a safe Xtream ${label}`);
}
}
export function toXtreamRawIteration(
value: XtreamValidatedIterationResult
): XtreamRawIterationResult {
const raw: Record<string, unknown> = { ...value };
delete raw['persistence'];
delete raw['validationStatus'];
return raw as unknown as XtreamRawIterationResult;
}
export function freezeDeep<T>(value: T): T {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.freeze(value);
Object.values(value).forEach(freezeDeep);
}
return value;
}
export function isExactRecord(
value: unknown,
keys: readonly string[]
): value is Record<string, unknown> {
try {
if (
typeof value !== 'object' ||
value === null ||
Array.isArray(value) ||
![Object.prototype, null].includes(Reflect.getPrototypeOf(value))
) {
return false;
}
const ownKeys = Reflect.ownKeys(value);
if (
ownKeys.length !== keys.length ||
ownKeys.some(
(key) => typeof key !== 'string' || !keys.includes(key)
)
) {
return false;
}
return keys.every((key) => {
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
return (
descriptor !== undefined &&
Object.prototype.hasOwnProperty.call(descriptor, 'value')
);
});
} catch {
return false;
}
}
export function isFiniteNonNegative(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
export function isSafeNonNegativeInteger(value: unknown): boolean {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
export function isPositiveSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) > 0;
}
export function isUtilization(value: unknown): boolean {
return isFiniteNonNegative(value) && value <= 1;
}
export function hasFiniteNonNegativeKeys(
value: Record<string, unknown>,
keys: readonly string[]
): boolean {
return keys.every((key) => isFiniteNonNegative(value[key]));
}
export function hasOrderedEventLoopDelay(
value: Record<string, unknown>
): boolean {
return (
(value['eventLoopDelayP95Ms'] as number) <=
(value['eventLoopDelayP99Ms'] as number) &&
(value['eventLoopDelayP99Ms'] as number) <=
(value['eventLoopDelayMaxMs'] as number)
);
}
export function hasMatchingEventLoopDelay(
worker: Record<string, unknown>
): boolean {
const requests = worker['requests'];
if (typeof requests !== 'object' || requests === null) return false;
return [
'eventLoopDelayMaxMs',
'eventLoopDelayP95Ms',
'eventLoopDelayP99Ms',
].every(
(key) => worker[key] === (requests as Record<string, unknown>)[key]
);
}
export function isExactXtreamProcessIdentity(value: unknown): boolean {
const keys = [
'captureGeneration',
'electronPid',
'freshProcess',
'generationIdentitySha256',
'launchId',
'profileDirectorySha256',
'startupAttemptCount',
'startupRetryReasons',
] as const;
if (!isExactRecord(value, keys)) return false;
const startupAttemptCount = isPositiveSafeInteger(
value['startupAttemptCount']
)
? Number(value['startupAttemptCount'])
: 0;
const startupRetryReasons = value['startupRetryReasons'];
return (
isPositiveSafeInteger(value['captureGeneration']) &&
isPositiveSafeInteger(value['electronPid']) &&
value['freshProcess'] === true &&
typeof value['launchId'] === 'string' &&
XTREAM_IDENTIFIER.test(value['launchId']) &&
typeof value['generationIdentitySha256'] === 'string' &&
XTREAM_SHA256.test(value['generationIdentitySha256']) &&
typeof value['profileDirectorySha256'] === 'string' &&
XTREAM_SHA256.test(value['profileDirectorySha256']) &&
(startupAttemptCount === 1 || startupAttemptCount === 2) &&
Array.isArray(startupRetryReasons) &&
startupRetryReasons.length === startupAttemptCount - 1 &&
startupRetryReasons.every(
(reason) =>
typeof reason === 'string' &&
Object.values(XTREAM_STARTUP_RETRY_REASON).includes(
reason as never
)
)
);
}
export function isXtreamProfile(
value: Record<string, unknown>,
kind: unknown
): boolean {
return kind === XTREAM_ITERATION_KIND.DIAGNOSTIC
? isPositiveSafeInteger(value['cpuProfileDurationMicros']) &&
value['cpuProfileUnavailableReason'] === null
: value['cpuProfileDurationMicros'] === null &&
value['cpuProfileUnavailableReason'] ===
XTREAM_NOT_APPLICABLE.PROFILE_OUTSIDE_DIAGNOSTIC;
}
export function sameValues(
left: readonly unknown[],
right: readonly unknown[]
): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
export function sameMaps(
left: ReadonlyMap<string, unknown>,
right: ReadonlyMap<string, unknown>
): boolean {
return sameValues([...left.entries()].sort(), [...right.entries()].sort());
}
function scheduleError(mode: 'formal' | 'smoke'): never {
throw new Error(
`Xtream benchmark requires exactly ${mode === 'formal' ? 'five' : 'one'} measured runs per scenario`
);
}
function invalidIterationCollection(): never {
throw new Error('invalid iteration collection');
}