mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
test(performance): add end-to-end Xtream benchmark harness (#1300)
* docs(performance): plan Xtream benchmark * feat(xtream-mock-server): add deterministic 100k fixture * style(xtream-mock-server): apply repository formatting * fix(xtream-mock-server): harden performance fixture data * feat(xtream-mock-server): add performance control plane * docs(performance): correct Xtream capture plan * fix(xtream-mock-server): harden performance controls * fix(xtream-mock-server): harden control lifecycle * feat(performance): add Xtream preload markers * feat(performance): trace Xtream main phases * feat(performance): mark Xtream store publications * feat(performance): trace Xtream database phases * feat(performance): trace Xtream delete cancellation * feat(performance): capture Xtream phase attribution * feat(performance): mark Sources Xtream refresh * test(performance): define Xtream benchmark evidence contracts * test(performance): add Xtream benchmark runner * test(performance): surface failure evidence writes * test(performance): align database read clock * test(performance): preserve capture failure contracts
This commit is contained in:
1 parent
5e725a06f3
commit
a2fafcfc08
256 files changed
+43760
-883
No files matched your search
@@ -87,8 +87,8 @@ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
|
||||
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events
|
||||
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
|
||||
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal
|
||||
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC request/completion markers plus count-only M3U acquire/parse/normalize and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset
|
||||
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization phase events, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
|
||||
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset
|
||||
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization plus Xtream category/content/cache-clear/delete/in-source-search phase events, profiling-only worker cancel-receipt acknowledgements, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
|
||||
|
||||
- Settings, portal request/response, and trace payloads must use
|
||||
`@iptvnator/shared/logging` or the redacting portal logger before reaching
|
||||
|
||||
@@ -142,8 +142,8 @@ Useful narrower flags:
|
||||
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow navigation/load lifecycle
|
||||
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
|
||||
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console logs into the Electron terminal
|
||||
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC request/completion markers plus count-only M3U acquire/parse/normalize and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset
|
||||
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization phase events, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
|
||||
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset
|
||||
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization plus Xtream category/content/cache-clear/delete/in-source-search phase events, profiling-only worker cancel-receipt acknowledgements, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
|
||||
|
||||
Settings, portal request/response, and trace payloads must use
|
||||
`@iptvnator/shared/logging` or the redacting portal logger before reaching
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
const controlToken =
|
||||
process.env['IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN'] ??
|
||||
randomBytes(32).toString('base64url');
|
||||
process.env['IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN'] = controlToken;
|
||||
|
||||
export default defineConfig({
|
||||
fullyParallel: false,
|
||||
reporter: [['list']],
|
||||
retries: 0,
|
||||
testDir: './src',
|
||||
testMatch: 'xtream.performance.ts',
|
||||
timeout: 30 * 60 * 1_000,
|
||||
use: {
|
||||
testIdAttribute: 'data-test-id',
|
||||
},
|
||||
webServer: {
|
||||
command: 'pnpm nx run xtream-mock-server:serve',
|
||||
cwd: workspaceRoot,
|
||||
env: {
|
||||
HOST: '127.0.0.1',
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL: '1',
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN: controlToken,
|
||||
PORT: '3221',
|
||||
},
|
||||
reuseExistingServer: false,
|
||||
url: 'http://127.0.0.1:3221/health',
|
||||
},
|
||||
workers: 1,
|
||||
});
|
||||
@@ -39,6 +39,16 @@
|
||||
"command": "pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-import.performance.ts"
|
||||
}
|
||||
},
|
||||
"benchmark-xtream": {
|
||||
"dependsOn": ["electron-backend:build-performance"],
|
||||
"executor": "nx:run-commands",
|
||||
"cache": false,
|
||||
"parallelism": false,
|
||||
"options": {
|
||||
"cwd": "apps/electron-backend-e2e",
|
||||
"command": "pnpm exec playwright test --config=playwright.xtream-performance.config.ts src/xtream.performance.ts"
|
||||
}
|
||||
},
|
||||
"packaged-frame-copy-smoke": {
|
||||
"dependsOn": ["test-packaged-frame-copy-fixtures"],
|
||||
"executor": "nx:run-commands",
|
||||
|
||||
@@ -77,7 +77,9 @@ type ElectronFixtures = {
|
||||
|
||||
export type LaunchElectronAppOptions = {
|
||||
args?: readonly string[];
|
||||
environmentInheritance?: 'all' | 'runtime-only';
|
||||
env?: Record<string, string | undefined>;
|
||||
omitEnvKeys?: readonly string[];
|
||||
};
|
||||
|
||||
export type PortalDebugEvent = {
|
||||
@@ -197,16 +199,7 @@ export async function launchElectronApp(
|
||||
|
||||
const electronApp = await electron.launch({
|
||||
args,
|
||||
env: {
|
||||
...process.env,
|
||||
// Electron E2E uses local mock HTTP servers for playlists, portals, and EPG sources.
|
||||
IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS:
|
||||
process.env['IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS'] ?? '1',
|
||||
...options.env,
|
||||
ELECTRON_IS_DEV: '0',
|
||||
IPTVNATOR_E2E_DATA_DIR: dataDir,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
env: buildElectronLaunchEnvironment(dataDir, options),
|
||||
});
|
||||
attachElectronProcessDiagnostics(electronApp);
|
||||
|
||||
@@ -222,6 +215,86 @@ export async function launchElectronApp(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildElectronLaunchEnvironment(
|
||||
dataDir: string,
|
||||
options: LaunchElectronAppOptions = {},
|
||||
inheritedEnvironment: NodeJS.ProcessEnv = process.env
|
||||
): Record<string, string> {
|
||||
if (
|
||||
options.environmentInheritance !== undefined &&
|
||||
options.environmentInheritance !== 'all' &&
|
||||
options.environmentInheritance !== 'runtime-only'
|
||||
) {
|
||||
throw new Error('Electron environment inheritance mode is invalid');
|
||||
}
|
||||
const runtimeOnly = options.environmentInheritance === 'runtime-only';
|
||||
const environment: Record<string, string | undefined> = {
|
||||
...(runtimeOnly
|
||||
? selectElectronRuntimeEnvironment(inheritedEnvironment)
|
||||
: inheritedEnvironment),
|
||||
// Electron E2E uses local mock HTTP servers for playlists, portals, and EPG sources.
|
||||
IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS: runtimeOnly
|
||||
? '1'
|
||||
: (inheritedEnvironment['IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS'] ??
|
||||
'1'),
|
||||
...options.env,
|
||||
ELECTRON_IS_DEV: '0',
|
||||
IPTVNATOR_E2E_DATA_DIR: dataDir,
|
||||
NODE_ENV: 'test',
|
||||
};
|
||||
for (const key of options.omitEnvKeys ?? []) {
|
||||
delete environment[key];
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(environment).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const ELECTRON_RUNTIME_ENVIRONMENT_KEYS = Object.freeze([
|
||||
'APPDATA',
|
||||
'COMSPEC',
|
||||
'DBUS_SESSION_BUS_ADDRESS',
|
||||
'DISPLAY',
|
||||
'HOME',
|
||||
'LANG',
|
||||
'LANGUAGE',
|
||||
'LC_ALL',
|
||||
'LC_CTYPE',
|
||||
'LOCALAPPDATA',
|
||||
'LOGNAME',
|
||||
'PATH',
|
||||
'PATHEXT',
|
||||
'SYSTEMROOT',
|
||||
'TEMP',
|
||||
'TMP',
|
||||
'TMPDIR',
|
||||
'USER',
|
||||
'USERPROFILE',
|
||||
'WAYLAND_DISPLAY',
|
||||
'WINDIR',
|
||||
'XAUTHORITY',
|
||||
'XDG_RUNTIME_DIR',
|
||||
'XDG_SESSION_TYPE',
|
||||
'__CF_USER_TEXT_ENCODING',
|
||||
] as const);
|
||||
|
||||
function selectElectronRuntimeEnvironment(
|
||||
inheritedEnvironment: NodeJS.ProcessEnv
|
||||
): Record<string, string> {
|
||||
const selected: Record<string, string> = {};
|
||||
const allowed = new Set(
|
||||
ELECTRON_RUNTIME_ENVIRONMENT_KEYS.map((key) => key.toUpperCase())
|
||||
);
|
||||
for (const [key, value] of Object.entries(inheritedEnvironment)) {
|
||||
if (typeof value === 'string' && allowed.has(key.toUpperCase())) {
|
||||
selected[key] = value;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the x64 unpacked Linux executable produced by electron-builder.
|
||||
* An explicit path wins so CI can point at an AppImage/Flatpak extraction
|
||||
@@ -1703,9 +1776,7 @@ export async function expectWorkspaceSearchStatus(
|
||||
}
|
||||
|
||||
/** The degraded-search hint chip must be absent (e.g. complete local search). */
|
||||
export async function expectNoWorkspaceSearchStatus(
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
export async function expectNoWorkspaceSearchStatus(page: Page): Promise<void> {
|
||||
await expect(
|
||||
page.locator('app-workspace-shell-header .search-chip--status')
|
||||
).toHaveCount(0);
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import { createDatabaseWorkerCancelCapture } from './database-worker-cancel-capture';
|
||||
|
||||
function dispatch(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
captureGeneration: 3,
|
||||
ipcCallId: 9,
|
||||
operationId: 'operation-1',
|
||||
postedEpochMs: 110,
|
||||
requestId: 'request-1',
|
||||
sourceEpochMs: 100,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function receipt(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
type: 'performance-cancel-received',
|
||||
epochMs: 120,
|
||||
operationId: 'operation-1',
|
||||
requestId: 'request-1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('database worker cancel capture', () => {
|
||||
it('restores without module closures for Electron-main injection', () => {
|
||||
const source = createDatabaseWorkerCancelCapture.toString();
|
||||
assert.doesNotMatch(source, /__name/);
|
||||
const factory = new Function(
|
||||
`"use strict"; return (${source});`
|
||||
)() as typeof createDatabaseWorkerCancelCapture;
|
||||
factory().start(1);
|
||||
});
|
||||
|
||||
it('keeps dispatch, worker receipt, and terminal cancellation distinct', () => {
|
||||
const capture = createDatabaseWorkerCancelCapture();
|
||||
capture.start(3);
|
||||
assert.equal(capture.acceptDispatch(dispatch()), true);
|
||||
assert.equal(
|
||||
capture.acceptReceipt({
|
||||
captureGeneration: 3,
|
||||
message: receipt(),
|
||||
receivedEpochMs: 121,
|
||||
recordGeneration: 3,
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptTerminal({
|
||||
captureGeneration: 3,
|
||||
operationId: 'operation-1',
|
||||
receivedEpochMs: 130,
|
||||
recordGeneration: 3,
|
||||
requestId: 'request-1',
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
const snapshot = capture.stop();
|
||||
assert.deepEqual(snapshot.invalidReasons, []);
|
||||
assert.deepEqual(
|
||||
snapshot.timeline.map(({ epochMs, type }) => ({ epochMs, type })),
|
||||
[
|
||||
{ epochMs: 110, type: 'db-cancel-dispatched' },
|
||||
{ epochMs: 120, type: 'db-cancel-received' },
|
||||
{ epochMs: 130, type: 'db-cancel-terminal-received' },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects extra fields, non-monotonic epochs, and cross-generation receipts', () => {
|
||||
for (const testCase of [
|
||||
{
|
||||
message: receipt({ credential: 'forbidden' }),
|
||||
receivedEpochMs: 121,
|
||||
recordGeneration: 3,
|
||||
},
|
||||
{
|
||||
message: receipt({ epochMs: 109 }),
|
||||
receivedEpochMs: 121,
|
||||
recordGeneration: 3,
|
||||
},
|
||||
{
|
||||
message: receipt({ epochMs: 122 }),
|
||||
receivedEpochMs: 121,
|
||||
recordGeneration: 3,
|
||||
},
|
||||
{
|
||||
message: receipt(),
|
||||
receivedEpochMs: 121,
|
||||
recordGeneration: 2,
|
||||
},
|
||||
]) {
|
||||
const capture = createDatabaseWorkerCancelCapture();
|
||||
capture.start(3);
|
||||
assert.equal(capture.acceptDispatch(dispatch()), true);
|
||||
assert.equal(
|
||||
capture.acceptReceipt({
|
||||
captureGeneration: 3,
|
||||
...testCase,
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert.notEqual(capture.stop().invalidReasons.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
it('invalidates a missing receipt separately from a missing terminal', () => {
|
||||
const missingReceipt = createDatabaseWorkerCancelCapture();
|
||||
missingReceipt.start(3);
|
||||
assert.equal(missingReceipt.acceptDispatch(dispatch()), true);
|
||||
assert.deepEqual(missingReceipt.stop().invalidReasons, [
|
||||
'cancel-receipt-missing',
|
||||
'cancel-terminal-missing',
|
||||
]);
|
||||
|
||||
const missingTerminal = createDatabaseWorkerCancelCapture();
|
||||
missingTerminal.start(3);
|
||||
assert.equal(missingTerminal.acceptDispatch(dispatch()), true);
|
||||
assert.equal(
|
||||
missingTerminal.acceptReceipt({
|
||||
captureGeneration: 3,
|
||||
message: receipt(),
|
||||
receivedEpochMs: 121,
|
||||
recordGeneration: 3,
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(missingTerminal.stop().invalidReasons, [
|
||||
'cancel-terminal-missing',
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails neutrally for revoked or throwing proxy input', () => {
|
||||
const capture = createDatabaseWorkerCancelCapture();
|
||||
capture.start(3);
|
||||
const revoked = Proxy.revocable({}, {});
|
||||
revoked.revoke();
|
||||
const throwing = new Proxy(
|
||||
{},
|
||||
{
|
||||
get() {
|
||||
throw new Error('property access denied');
|
||||
},
|
||||
ownKeys() {
|
||||
throw new Error('keys denied');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() => capture.acceptDispatch(revoked.proxy));
|
||||
assert.equal(capture.acceptDispatch(revoked.proxy), false);
|
||||
assert.doesNotThrow(() => capture.acceptReceipt(throwing as never));
|
||||
assert.equal(capture.acceptReceipt(throwing as never), false);
|
||||
assert.doesNotThrow(() => capture.acceptTerminal(throwing as never));
|
||||
assert.equal(capture.acceptTerminal(throwing as never), false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,295 @@
|
||||
export interface DatabaseWorkerCancelTimelineRecord {
|
||||
readonly epochMs: number;
|
||||
readonly ipcCallId: number;
|
||||
readonly operationId: string;
|
||||
readonly requestId: string;
|
||||
readonly sourceEpochMs: number;
|
||||
readonly type:
|
||||
| 'db-cancel-dispatched'
|
||||
| 'db-cancel-received'
|
||||
| 'db-cancel-terminal-received';
|
||||
}
|
||||
|
||||
export interface DatabaseWorkerCancelCaptureSnapshot {
|
||||
readonly invalidReasons: readonly string[];
|
||||
readonly timeline: readonly DatabaseWorkerCancelTimelineRecord[];
|
||||
}
|
||||
|
||||
export interface DatabaseWorkerCancelCapture {
|
||||
acceptDispatch(input: unknown): boolean;
|
||||
acceptReceipt(input: {
|
||||
readonly captureGeneration: number;
|
||||
readonly message: unknown;
|
||||
readonly receivedEpochMs: number;
|
||||
readonly recordGeneration: number | null;
|
||||
}): boolean;
|
||||
acceptTerminal(input: {
|
||||
readonly captureGeneration: number;
|
||||
readonly operationId: string;
|
||||
readonly receivedEpochMs: number;
|
||||
readonly recordGeneration: number | null;
|
||||
readonly requestId: string;
|
||||
}): boolean;
|
||||
start(captureGeneration: number): void;
|
||||
stop(): DatabaseWorkerCancelCaptureSnapshot;
|
||||
}
|
||||
|
||||
export function createDatabaseWorkerCancelCapture(): DatabaseWorkerCancelCapture {
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
interface DispatchState {
|
||||
readonly captureGeneration: number;
|
||||
readonly ipcCallId: number;
|
||||
readonly operationId: string;
|
||||
readonly postedEpochMs: number;
|
||||
readonly requestId: string;
|
||||
readonly sourceEpochMs: number;
|
||||
receiptEpochMs: number | null;
|
||||
terminalEpochMs: number | null;
|
||||
}
|
||||
|
||||
const dispatchKeys = new Set([
|
||||
'captureGeneration',
|
||||
'ipcCallId',
|
||||
'operationId',
|
||||
'postedEpochMs',
|
||||
'requestId',
|
||||
'sourceEpochMs',
|
||||
]);
|
||||
const receiptKeys = new Set([
|
||||
'type',
|
||||
'epochMs',
|
||||
'operationId',
|
||||
'requestId',
|
||||
]);
|
||||
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
let active = false;
|
||||
let generation = 0;
|
||||
let invalidReasons: string[] = [];
|
||||
let timeline: DatabaseWorkerCancelTimelineRecord[] = [];
|
||||
const dispatches = new Map<string, DispatchState>();
|
||||
const helpers = {
|
||||
invalidate(reason: string): false {
|
||||
invalidReasons.push(reason);
|
||||
return false;
|
||||
},
|
||||
isEpoch(value: unknown): value is number {
|
||||
return (
|
||||
typeof value === 'number' && Number.isFinite(value) && value > 0
|
||||
);
|
||||
},
|
||||
isGeneration(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0;
|
||||
},
|
||||
isIdentifier(value: unknown): value is string {
|
||||
return typeof value === 'string' && identifierPattern.test(value);
|
||||
},
|
||||
isRecord(value: unknown): value is JsonRecord {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
);
|
||||
},
|
||||
ownsOnly(value: JsonRecord, keys: ReadonlySet<string>): boolean {
|
||||
const ownKeys = Reflect.ownKeys(value);
|
||||
return (
|
||||
ownKeys.length === keys.size &&
|
||||
ownKeys.every((key) => typeof key === 'string' && keys.has(key))
|
||||
);
|
||||
},
|
||||
timelineRecord(
|
||||
dispatch: DispatchState,
|
||||
epochMs: number,
|
||||
type: DatabaseWorkerCancelTimelineRecord['type']
|
||||
): DatabaseWorkerCancelTimelineRecord {
|
||||
return {
|
||||
epochMs,
|
||||
ipcCallId: dispatch.ipcCallId,
|
||||
operationId: dispatch.operationId,
|
||||
requestId: dispatch.requestId,
|
||||
sourceEpochMs: dispatch.sourceEpochMs,
|
||||
type,
|
||||
};
|
||||
},
|
||||
};
|
||||
return {
|
||||
acceptDispatch(input): boolean {
|
||||
try {
|
||||
if (
|
||||
!active ||
|
||||
!helpers.isRecord(input) ||
|
||||
!helpers.ownsOnly(input, dispatchKeys) ||
|
||||
input['captureGeneration'] !== generation ||
|
||||
!helpers.isGeneration(input['captureGeneration']) ||
|
||||
!Number.isSafeInteger(input['ipcCallId']) ||
|
||||
Number(input['ipcCallId']) <= 0 ||
|
||||
!helpers.isIdentifier(input['operationId']) ||
|
||||
!helpers.isIdentifier(input['requestId']) ||
|
||||
!helpers.isEpoch(input['postedEpochMs']) ||
|
||||
!helpers.isEpoch(input['sourceEpochMs']) ||
|
||||
Number(input['sourceEpochMs']) >
|
||||
Number(input['postedEpochMs'])
|
||||
) {
|
||||
return helpers.invalidate('cancel-dispatch-invalid');
|
||||
}
|
||||
const requestId = input['requestId'];
|
||||
if (dispatches.has(requestId)) {
|
||||
return helpers.invalidate('cancel-dispatch-duplicate');
|
||||
}
|
||||
const dispatch = {
|
||||
captureGeneration: generation,
|
||||
ipcCallId: Number(input['ipcCallId']),
|
||||
operationId: input['operationId'],
|
||||
postedEpochMs: Number(input['postedEpochMs']),
|
||||
receiptEpochMs: null,
|
||||
requestId,
|
||||
sourceEpochMs: Number(input['sourceEpochMs']),
|
||||
terminalEpochMs: null,
|
||||
};
|
||||
dispatches.set(requestId, dispatch);
|
||||
timeline.push(
|
||||
helpers.timelineRecord(
|
||||
dispatch,
|
||||
dispatch.postedEpochMs,
|
||||
'db-cancel-dispatched'
|
||||
)
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return helpers.invalidate('cancel-dispatch-invalid');
|
||||
}
|
||||
},
|
||||
acceptReceipt(input): boolean {
|
||||
try {
|
||||
if (
|
||||
!active ||
|
||||
input.captureGeneration !== generation ||
|
||||
input.recordGeneration !== generation
|
||||
) {
|
||||
return helpers.invalidate(
|
||||
'cancel-receipt-generation-mismatch'
|
||||
);
|
||||
}
|
||||
const message = input.message;
|
||||
if (
|
||||
!helpers.isRecord(message) ||
|
||||
!helpers.ownsOnly(message, receiptKeys) ||
|
||||
message['type'] !== 'performance-cancel-received' ||
|
||||
!helpers.isIdentifier(message['operationId']) ||
|
||||
!helpers.isIdentifier(message['requestId']) ||
|
||||
!helpers.isEpoch(message['epochMs']) ||
|
||||
!helpers.isEpoch(input.receivedEpochMs)
|
||||
) {
|
||||
return helpers.invalidate('cancel-receipt-invalid-schema');
|
||||
}
|
||||
const dispatch = dispatches.get(message['requestId']);
|
||||
if (!dispatch) {
|
||||
return helpers.invalidate(
|
||||
'cancel-receipt-dispatch-missing'
|
||||
);
|
||||
}
|
||||
if (dispatch.operationId !== message['operationId']) {
|
||||
return helpers.invalidate(
|
||||
'cancel-receipt-correlation-mismatch'
|
||||
);
|
||||
}
|
||||
if (
|
||||
Number(message['epochMs']) < dispatch.postedEpochMs ||
|
||||
Number(message['epochMs']) > input.receivedEpochMs
|
||||
) {
|
||||
return helpers.invalidate('cancel-receipt-epoch-invalid');
|
||||
}
|
||||
if (dispatch.receiptEpochMs !== null) {
|
||||
return helpers.invalidate('cancel-receipt-duplicate');
|
||||
}
|
||||
dispatch.receiptEpochMs = Number(message['epochMs']);
|
||||
timeline.push(
|
||||
helpers.timelineRecord(
|
||||
dispatch,
|
||||
dispatch.receiptEpochMs,
|
||||
'db-cancel-received'
|
||||
)
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return helpers.invalidate('cancel-receipt-invalid-schema');
|
||||
}
|
||||
},
|
||||
acceptTerminal(input): boolean {
|
||||
try {
|
||||
if (
|
||||
!active ||
|
||||
input.captureGeneration !== generation ||
|
||||
input.recordGeneration !== generation
|
||||
) {
|
||||
return helpers.invalidate(
|
||||
'cancel-terminal-generation-mismatch'
|
||||
);
|
||||
}
|
||||
if (
|
||||
!helpers.isIdentifier(input.operationId) ||
|
||||
!helpers.isIdentifier(input.requestId) ||
|
||||
!helpers.isEpoch(input.receivedEpochMs)
|
||||
) {
|
||||
return helpers.invalidate('cancel-terminal-invalid');
|
||||
}
|
||||
const dispatch = dispatches.get(input.requestId);
|
||||
if (!dispatch) {
|
||||
return helpers.invalidate(
|
||||
'cancel-terminal-dispatch-missing'
|
||||
);
|
||||
}
|
||||
if (dispatch.operationId !== input.operationId) {
|
||||
return helpers.invalidate(
|
||||
'cancel-terminal-correlation-mismatch'
|
||||
);
|
||||
}
|
||||
if (
|
||||
dispatch.receiptEpochMs === null ||
|
||||
input.receivedEpochMs < dispatch.receiptEpochMs
|
||||
) {
|
||||
return helpers.invalidate('cancel-terminal-before-receipt');
|
||||
}
|
||||
if (dispatch.terminalEpochMs !== null) {
|
||||
return helpers.invalidate('cancel-terminal-duplicate');
|
||||
}
|
||||
dispatch.terminalEpochMs = input.receivedEpochMs;
|
||||
timeline.push(
|
||||
helpers.timelineRecord(
|
||||
dispatch,
|
||||
dispatch.terminalEpochMs,
|
||||
'db-cancel-terminal-received'
|
||||
)
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return helpers.invalidate('cancel-terminal-invalid');
|
||||
}
|
||||
},
|
||||
start(captureGeneration): void {
|
||||
if (!helpers.isGeneration(captureGeneration)) {
|
||||
throw new Error('database-worker-cancel-generation-invalid');
|
||||
}
|
||||
active = true;
|
||||
generation = captureGeneration;
|
||||
invalidReasons = [];
|
||||
timeline = [];
|
||||
dispatches.clear();
|
||||
},
|
||||
stop(): DatabaseWorkerCancelCaptureSnapshot {
|
||||
for (const dispatch of dispatches.values()) {
|
||||
if (dispatch.receiptEpochMs === null) {
|
||||
invalidReasons.push('cancel-receipt-missing');
|
||||
}
|
||||
if (dispatch.terminalEpochMs === null) {
|
||||
invalidReasons.push('cancel-terminal-missing');
|
||||
}
|
||||
}
|
||||
active = false;
|
||||
return Object.freeze({
|
||||
invalidReasons: Object.freeze([...invalidReasons]),
|
||||
timeline: Object.freeze([...timeline]),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
+5
-4
@@ -370,21 +370,22 @@ test('worker capture failures use an artifact-neutral timeline label', () => {
|
||||
assert.doesNotMatch(source, /worker-profile-error:\$\{stage\}/);
|
||||
});
|
||||
|
||||
test('capture generation resets artifact paths and always emits current DB records', () => {
|
||||
test('capture generation resets artifact paths while arming and always emits current DB records', () => {
|
||||
const source = readFileSync(
|
||||
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const start = source.indexOf('const startCapture = async (');
|
||||
const diagnostic = source.indexOf('if (state.diagnostic)', start);
|
||||
const api = source.indexOf('const api =', start);
|
||||
const profileReset = source.indexOf('state.mainProfilePath = null', start);
|
||||
const snapshotReset = source.indexOf(
|
||||
'state.mainSnapshotPath = null',
|
||||
start
|
||||
);
|
||||
|
||||
assert.ok(profileReset > start && profileReset < diagnostic);
|
||||
assert.ok(snapshotReset > start && snapshotReset < diagnostic);
|
||||
assert.ok(profileReset > start && profileReset < api);
|
||||
assert.ok(snapshotReset > start && snapshotReset < api);
|
||||
assert.doesNotMatch(source.slice(start, api), /'Profiler\.start'/);
|
||||
assert.match(
|
||||
source,
|
||||
/record\.captureGeneration === state\.captureGeneration[\s\S]*record\.kind === 'database\.worker'/
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
buildElectronLaunchEnvironment,
|
||||
ELECTRON_RUNTIME_ENVIRONMENT_KEYS,
|
||||
} from '../electron-test-fixtures';
|
||||
import { assertXtreamBenchmarkEnvironmentIsolated } from './xtream-benchmark-iteration-support';
|
||||
|
||||
describe('Electron launch environment isolation', () => {
|
||||
it('inherits only portable runtime keys for a profiled benchmark child', () => {
|
||||
const environment = buildElectronLaunchEnvironment(
|
||||
'/tmp/iptvnator-performance',
|
||||
{
|
||||
environmentInheritance: 'runtime-only',
|
||||
env: {
|
||||
IPTVNATOR_PERF_CAPTURE: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
AWS_SECRET_ACCESS_KEY: 'aws-secret',
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
GITHUB_TOKEN: 'github-secret',
|
||||
HTTPS_PROXY: 'https://user:password@proxy.invalid',
|
||||
IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS: '0',
|
||||
IPTVNATOR_REAL_PROVIDER_URL: 'https://provider.invalid',
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL: '1',
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN: 'secret-token',
|
||||
NODE_OPTIONS: '--require credential-dumper.js',
|
||||
PATH: '/usr/bin',
|
||||
REAL_PROVIDER_PASSWORD: 'provider-password',
|
||||
REAL_PROVIDER_URL: 'https://provider.invalid',
|
||||
TMPDIR: '/tmp/runtime',
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(environment['IPTVNATOR_PERF_CAPTURE'], '1');
|
||||
assert.equal(environment['IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS'], '1');
|
||||
assert.equal(environment['PATH'], '/usr/bin');
|
||||
assert.equal(environment['TMPDIR'], '/tmp/runtime');
|
||||
for (const key of [
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'ELECTRON_RUN_AS_NODE',
|
||||
'GITHUB_TOKEN',
|
||||
'HTTPS_PROXY',
|
||||
'IPTVNATOR_REAL_PROVIDER_URL',
|
||||
'IPTVNATOR_XTREAM_MOCK_CONTROL',
|
||||
'IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN',
|
||||
'NODE_OPTIONS',
|
||||
'REAL_PROVIDER_PASSWORD',
|
||||
'REAL_PROVIDER_URL',
|
||||
]) {
|
||||
assert.equal(environment[key], undefined, key);
|
||||
}
|
||||
});
|
||||
|
||||
it('drops undefined overrides instead of serializing them into the child', () => {
|
||||
const environment = buildElectronLaunchEnvironment(
|
||||
'/tmp/iptvnator-performance',
|
||||
{ env: { OPTIONAL_DEBUG_FLAG: undefined } },
|
||||
{ OPTIONAL_DEBUG_FLAG: 'inherited' }
|
||||
);
|
||||
|
||||
assert.equal(environment['OPTIONAL_DEBUG_FLAG'], undefined);
|
||||
});
|
||||
|
||||
it('checks the effective Electron main environment, not only launch input', async () => {
|
||||
const allowed = [
|
||||
...ELECTRON_RUNTIME_ENVIRONMENT_KEYS,
|
||||
'ELECTRON_IS_DEV',
|
||||
'IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS',
|
||||
'IPTVNATOR_DB_WORKER_BATCH_DELAY_MS',
|
||||
'IPTVNATOR_E2E_DATA_DIR',
|
||||
'IPTVNATOR_PERF_CAPTURE',
|
||||
'IPTVNATOR_PERF_WORKER_PROFILING',
|
||||
'IPTVNATOR_TRACE_RENDERER_CONSOLE',
|
||||
'NODE_ENV',
|
||||
];
|
||||
|
||||
await assert.doesNotReject(
|
||||
assertXtreamBenchmarkEnvironmentIsolated(fakeApp(allowed))
|
||||
);
|
||||
for (const leaked of [
|
||||
'GITHUB_TOKEN',
|
||||
'HTTPS_PROXY',
|
||||
'NODE_OPTIONS',
|
||||
'REAL_PROVIDER_URL',
|
||||
]) {
|
||||
await assert.rejects(
|
||||
assertXtreamBenchmarkEnvironmentIsolated(
|
||||
fakeApp([...allowed, leaked])
|
||||
),
|
||||
/xtream-benchmark-environment-reached-electron/
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function fakeApp(keys: readonly string[]) {
|
||||
return {
|
||||
electronApp: {
|
||||
evaluate: async () => [...keys],
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
+8
@@ -73,6 +73,14 @@ export function isCpuProfile(value: unknown): boolean {
|
||||
return elapsed > 0;
|
||||
}
|
||||
|
||||
export function readCpuProfileDurationMicros(value: unknown): number | null {
|
||||
if (!isCpuProfile(value)) {
|
||||
return null;
|
||||
}
|
||||
const profile = value as Record<string, number>;
|
||||
return profile['endTime'] - profile['startTime'];
|
||||
}
|
||||
|
||||
export function isHeapSnapshot(value: unknown): boolean {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
|
||||
@@ -69,7 +69,10 @@ export async function resolveM3uImportBenchmarkConfiguration(): Promise<M3uImpor
|
||||
);
|
||||
await assertMissing(layout.variantDirectory);
|
||||
await assertPerformanceArtifactCapacity(layout.variantDirectory);
|
||||
await mkdir(layout.variantDirectory, { recursive: true });
|
||||
await mkdir(layout.variantDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: true,
|
||||
});
|
||||
return Object.freeze({
|
||||
...layout,
|
||||
electronVersion: electronPackage.version,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { once } from 'node:events';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { finished } from 'node:stream/promises';
|
||||
|
||||
import type { CDPSession } from '@playwright/test';
|
||||
|
||||
@@ -9,6 +9,11 @@ interface TraceData {
|
||||
readonly value: readonly unknown[];
|
||||
}
|
||||
|
||||
export interface RendererHeapUsageSample {
|
||||
readonly epochMs: number;
|
||||
readonly usedSize: number;
|
||||
}
|
||||
|
||||
export interface RendererArtifactCapture {
|
||||
beginTraceOutput(): void;
|
||||
readonly cpuProfilePath: string | null;
|
||||
@@ -27,7 +32,9 @@ export function createRendererArtifactCapture(options: {
|
||||
const tracePath = options.diagnostic
|
||||
? join(options.outputDirectory, 'renderer.trace.json')
|
||||
: null;
|
||||
const traceOutput = tracePath ? createWriteStream(tracePath) : null;
|
||||
const traceOutput = tracePath
|
||||
? createWriteStream(tracePath, { flags: 'wx', mode: 0o600 })
|
||||
: null;
|
||||
let traceOutputOpened = false;
|
||||
let traceOutputFinish: Promise<void> | null = null;
|
||||
let traceFirstEvent = true;
|
||||
@@ -62,9 +69,7 @@ export function createRendererArtifactCapture(options: {
|
||||
traceOutput.write(']}');
|
||||
traceOutput.end();
|
||||
}
|
||||
if (!traceOutput.writableFinished) {
|
||||
await once(traceOutput, 'finish');
|
||||
}
|
||||
await finished(traceOutput);
|
||||
})();
|
||||
return traceOutputFinish;
|
||||
},
|
||||
@@ -122,7 +127,7 @@ export async function stopRendererDiagnosticCapture(
|
||||
await writeFile(
|
||||
requirePath(artifacts.cpuProfilePath, 'renderer CPU profile'),
|
||||
JSON.stringify(cpuProfile['profile']),
|
||||
'utf8'
|
||||
{ encoding: 'utf8', flag: 'wx', mode: 0o600 }
|
||||
);
|
||||
await session.send('Tracing.end');
|
||||
await artifacts.traceComplete;
|
||||
@@ -145,7 +150,8 @@ export async function takeRendererHeapSnapshot(
|
||||
path: string | null
|
||||
): Promise<void> {
|
||||
const output = createWriteStream(
|
||||
requirePath(path, 'renderer heap snapshot')
|
||||
requirePath(path, 'renderer heap snapshot'),
|
||||
{ flags: 'wx', mode: 0o600 }
|
||||
);
|
||||
const onChunk = ({ chunk }: { readonly chunk: string }): void => {
|
||||
output.write(chunk);
|
||||
@@ -158,9 +164,43 @@ export async function takeRendererHeapSnapshot(
|
||||
} finally {
|
||||
session.off('HeapProfiler.addHeapSnapshotChunk', onChunk);
|
||||
output.end();
|
||||
if (!output.writableFinished) {
|
||||
await once(output, 'finish');
|
||||
}
|
||||
await finished(output);
|
||||
}
|
||||
}
|
||||
|
||||
export function selectRendererPeakHeapUsedBytes(
|
||||
samples: readonly RendererHeapUsageSample[],
|
||||
measurementStartEpochMs: number,
|
||||
terminalEpochMs: number
|
||||
): number {
|
||||
const inWindow = samples.filter(
|
||||
({ epochMs, usedSize }) =>
|
||||
Number.isFinite(epochMs) &&
|
||||
epochMs >= measurementStartEpochMs &&
|
||||
epochMs <= terminalEpochMs &&
|
||||
Number.isFinite(usedSize) &&
|
||||
usedSize >= 0
|
||||
);
|
||||
if (inWindow.length === 0) {
|
||||
throw new Error('renderer-heap-window-empty');
|
||||
}
|
||||
return Math.max(...inWindow.map(({ usedSize }) => usedSize));
|
||||
}
|
||||
|
||||
export function assertRendererMeasurementWindow(input: {
|
||||
readonly captureCutoffEpochMs: number;
|
||||
readonly measurementStartEpochMs: number;
|
||||
readonly operationStartEpochMs: number;
|
||||
readonly terminalEpochMs: number;
|
||||
}): void {
|
||||
const epochs = Object.values(input);
|
||||
if (
|
||||
!epochs.every((epoch) => Number.isFinite(epoch) && epoch > 0) ||
|
||||
input.measurementStartEpochMs < input.operationStartEpochMs ||
|
||||
input.terminalEpochMs < input.measurementStartEpochMs ||
|
||||
input.captureCutoffEpochMs < input.terminalEpochMs
|
||||
) {
|
||||
throw new Error('renderer-measurement-window-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,10 @@ async function runScenario(
|
||||
assertSyntheticM3uImportUrl(server.resourceUrl);
|
||||
await server.prime();
|
||||
await assertPerformanceArtifactCapacity(scenarioDirectory);
|
||||
await mkdir(scenarioDirectory, { recursive: false });
|
||||
await mkdir(scenarioDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: false,
|
||||
});
|
||||
for (const definition of definitions) {
|
||||
console.log(
|
||||
`[performance] ${config.variant}/${scenario.id}/${definition.runId} starting`
|
||||
@@ -163,7 +166,10 @@ async function runIteration(
|
||||
): Promise<M3uImportIterationResult> {
|
||||
const iterationDirectory = join(scenarioDirectory, definition.runId);
|
||||
await assertPerformanceArtifactCapacity(iterationDirectory);
|
||||
await mkdir(iterationDirectory, { recursive: false });
|
||||
await mkdir(iterationDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: false,
|
||||
});
|
||||
const dataDirectory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-m3u-import-performance-')
|
||||
);
|
||||
@@ -179,6 +185,7 @@ async function runIteration(
|
||||
`--remote-debugging-port=${config.rendererCdpPort}`,
|
||||
`--user-data-dir=${join(dataDirectory, 'user-data')}`,
|
||||
],
|
||||
environmentInheritance: 'runtime-only',
|
||||
env: {
|
||||
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
|
||||
IPTVNATOR_PERF_CAPTURE: '1',
|
||||
@@ -188,10 +195,7 @@ async function runIteration(
|
||||
},
|
||||
});
|
||||
app.mainWindow.setDefaultTimeout(120_000);
|
||||
await assertRendererCdpTarget(
|
||||
app.mainWindow,
|
||||
config.rendererCdpPort
|
||||
);
|
||||
await assertRendererCdpTarget(app.mainWindow, config.rendererCdpPort);
|
||||
await installMainCapture(app.electronApp);
|
||||
const rendererWindowIdentity = await resolveRendererWindowIdentity(app);
|
||||
const requestsBefore = server.requestCount();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RendererProcessRssCapture } from './renderer-process-rss-capture';
|
||||
import type { RendererRssValidity } from './renderer-rss-validity';
|
||||
import type {
|
||||
AppPlaylistPerformancePhase,
|
||||
DatabaseWorkerPerformancePhase,
|
||||
PerformancePhaseEvent,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
@@ -57,8 +57,7 @@ export type WorkerPostGcHeapCapture =
|
||||
};
|
||||
|
||||
export const WORKER_PEAK_MEMORY_UNAVAILABLE_REASON = {
|
||||
EXTERNAL_MEMORY_SAMPLES_MISSING:
|
||||
'worker-external-memory-samples-missing',
|
||||
EXTERNAL_MEMORY_SAMPLES_MISSING: 'worker-external-memory-samples-missing',
|
||||
HEAP_USED_SAMPLES_MISSING: 'worker-heap-used-samples-missing',
|
||||
} as const;
|
||||
|
||||
@@ -86,17 +85,23 @@ export interface ProcessMemoryMetrics {
|
||||
}
|
||||
|
||||
export interface MainTimelineRecord {
|
||||
readonly action?: string | null;
|
||||
readonly boundary?: 'end' | 'start';
|
||||
readonly byteCount?: number;
|
||||
readonly categoryType?: string | null;
|
||||
readonly contentType?: string | null;
|
||||
readonly durationMs?: number | null;
|
||||
readonly epochMs: number;
|
||||
readonly ipcCallId?: number;
|
||||
readonly itemCount?: number;
|
||||
readonly itemCount?: number | null;
|
||||
readonly method?: string;
|
||||
readonly operation?: string;
|
||||
readonly operationId?: string;
|
||||
readonly operationId?: string | null;
|
||||
readonly outcome?: 'error' | 'success' | null;
|
||||
readonly phase?: string;
|
||||
readonly playlistId?: string;
|
||||
readonly playlistId?: string | null;
|
||||
readonly requestId?: string;
|
||||
readonly sessionId?: string | null;
|
||||
readonly success?: boolean;
|
||||
readonly sourceEpochMs?: number;
|
||||
readonly type: string;
|
||||
@@ -114,7 +119,7 @@ export interface WorkerRequestPerformanceMetrics {
|
||||
readonly operationId: string | null;
|
||||
readonly operationIdUnavailableReason: string | null;
|
||||
readonly performanceCaptureUnavailableReason: string | null;
|
||||
readonly phaseEvents: readonly PerformancePhaseEvent<AppPlaylistPerformancePhase>[];
|
||||
readonly phaseEvents: readonly PerformancePhaseEvent<DatabaseWorkerPerformancePhase>[];
|
||||
readonly playlistId: string | null;
|
||||
readonly requestId: string | null;
|
||||
readonly requestReceivedEpochMs: number | null;
|
||||
|
||||
@@ -186,7 +186,10 @@ async function runIteration(
|
||||
): Promise<CancellationIterationResult> {
|
||||
const iterationDirectory = join(config.outputDirectory, definition.runId);
|
||||
await assertPerformanceArtifactCapacity(iterationDirectory);
|
||||
await mkdir(iterationDirectory, { recursive: false });
|
||||
await mkdir(iterationDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: false,
|
||||
});
|
||||
const dataDirectory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-m3u-performance-')
|
||||
);
|
||||
@@ -204,6 +207,7 @@ async function runIteration(
|
||||
`--remote-debugging-port=${RENDERER_CDP_PORT}`,
|
||||
`--user-data-dir=${join(dataDirectory, 'user-data')}`,
|
||||
],
|
||||
environmentInheritance: 'runtime-only',
|
||||
env: {
|
||||
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
|
||||
IPTVNATOR_PERF_CAPTURE: '1',
|
||||
@@ -448,7 +452,10 @@ async function resolveConfiguration(): Promise<BenchmarkConfiguration> {
|
||||
const outputDirectory = join(outputRoot, variant);
|
||||
await assertMissing(outputDirectory);
|
||||
await assertPerformanceArtifactCapacity(outputDirectory);
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
await mkdir(outputDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: true,
|
||||
});
|
||||
return Object.freeze({
|
||||
electronVersion: electronPackage.version,
|
||||
gitCommit: sourceState.commit,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable max-lines -- Renderer CDP and injected-page lifecycle are kept together so raw artifact boundaries stay auditable. */
|
||||
import { once } from 'node:events';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { finished } from 'node:stream/promises';
|
||||
|
||||
import type { CDPSession, Page } from '@playwright/test';
|
||||
|
||||
@@ -106,7 +106,8 @@ export async function startRendererCapture(
|
||||
await session.send('Profiler.enable');
|
||||
await session.send('Profiler.start');
|
||||
traceOutput = createWriteStream(
|
||||
requireArtifactPath(tracePath, 'renderer trace')
|
||||
requireArtifactPath(tracePath, 'renderer trace'),
|
||||
{ flags: 'wx', mode: 0o600 }
|
||||
);
|
||||
traceOutput.write('{"traceEvents":[');
|
||||
session.on('Tracing.dataCollected', onTraceData);
|
||||
@@ -143,7 +144,7 @@ export async function startRendererCapture(
|
||||
await writeFile(
|
||||
requireArtifactPath(cpuProfilePath, 'renderer CPU profile'),
|
||||
JSON.stringify(cpuProfile['profile']),
|
||||
'utf8'
|
||||
{ encoding: 'utf8', flag: 'wx', mode: 0o600 }
|
||||
);
|
||||
await session.send('Tracing.end');
|
||||
await traceComplete;
|
||||
@@ -152,7 +153,7 @@ export async function startRendererCapture(
|
||||
traceOutput?.write(']}');
|
||||
traceOutput?.end();
|
||||
if (traceOutput) {
|
||||
await once(traceOutput, 'finish');
|
||||
await finished(traceOutput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +168,8 @@ export async function startRendererCapture(
|
||||
requireArtifactPath(
|
||||
heapSnapshotPath,
|
||||
'renderer heap snapshot'
|
||||
)
|
||||
),
|
||||
{ flags: 'wx', mode: 0o600 }
|
||||
);
|
||||
const onSnapshotChunk = (message: {
|
||||
readonly chunk: string;
|
||||
@@ -186,7 +188,7 @@ export async function startRendererCapture(
|
||||
onSnapshotChunk
|
||||
);
|
||||
output.end();
|
||||
await once(output, 'finish');
|
||||
await finished(output);
|
||||
}
|
||||
|
||||
await session.detach();
|
||||
@@ -211,8 +213,7 @@ export async function startRendererCapture(
|
||||
unknown
|
||||
>;
|
||||
const state = target[stateKey] as
|
||||
| { cancelClickEpochMs: number | null }
|
||||
| undefined;
|
||||
{ cancelClickEpochMs: number | null } | undefined;
|
||||
return typeof state?.cancelClickEpochMs === 'number';
|
||||
},
|
||||
RENDERER_CAPTURE_STATE_KEY,
|
||||
@@ -228,8 +229,7 @@ export async function startRendererCapture(
|
||||
unknown
|
||||
>;
|
||||
const state = target[stateKey] as
|
||||
| { uiPaintedEpochMs: number | null }
|
||||
| undefined;
|
||||
{ uiPaintedEpochMs: number | null } | undefined;
|
||||
return typeof state?.uiPaintedEpochMs === 'number';
|
||||
},
|
||||
RENDERER_CAPTURE_STATE_KEY,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
function section(startMarker: string, endMarker: string): string {
|
||||
const start = source.indexOf(startMarker);
|
||||
const end = source.indexOf(endMarker, start);
|
||||
assert.ok(start >= 0, `missing start marker: ${startMarker}`);
|
||||
assert.ok(end > start, `missing end marker: ${endMarker}`);
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('samples main and worker memory from one shared interval scheduler', () => {
|
||||
const workerRecord = section(
|
||||
'interface WorkerRecord',
|
||||
'interface TimelineRecord'
|
||||
);
|
||||
const resetWorker = section(
|
||||
'const resetWorkerForCapture',
|
||||
'const startWorker'
|
||||
);
|
||||
const startWorker = section(
|
||||
'const startWorker',
|
||||
'const stopWorkerSampling'
|
||||
);
|
||||
const stopWorker = section(
|
||||
'const stopWorkerSampling',
|
||||
'const joinFinalWorkerSample'
|
||||
);
|
||||
const joinFinalWorker = section(
|
||||
'const joinFinalWorkerSample',
|
||||
'const stopWorkerProfile'
|
||||
);
|
||||
const sampleMain = section('const sampleMain', 'const startCapture');
|
||||
const beginMeasurement = section(
|
||||
'const beginMeasurement = async',
|
||||
'const startCapture'
|
||||
);
|
||||
const armCapture = section('const startCapture', 'const api =');
|
||||
const stopCapture = section(
|
||||
'stop: async (',
|
||||
'xtreamStatus: (): XtreamMainCaptureStatus'
|
||||
);
|
||||
|
||||
assert.match(workerRecord, /samplingStarted: boolean/);
|
||||
assert.doesNotMatch(workerRecord, /sampleTimer/);
|
||||
assert.match(resetWorker, /record\.samplingStarted = false/);
|
||||
assert.match(startWorker, /if \(record\.samplingStarted\)/);
|
||||
assert.match(
|
||||
startWorker,
|
||||
/record\.samplingStarted = true[\s\S]*const initialSample = sampleWorker\(record\)/
|
||||
);
|
||||
assert.doesNotMatch(startWorker, /setInterval/);
|
||||
assert.match(stopWorker, /record\.samplingStarted = false/);
|
||||
assert.match(joinFinalWorker, /await sampleWorker\(record\)/);
|
||||
assert.match(
|
||||
sampleMain,
|
||||
/for \(const record of records\.values\(\)\)[\s\S]*record\.samplingStarted &&[\s\S]*isCurrentCaptureRecord\(record\)[\s\S]*void sampleWorker\(record\)/
|
||||
);
|
||||
assert.match(
|
||||
beginMeasurement,
|
||||
/sampleMain\(\)[\s\S]*state\.sampleTimer = setInterval\(sampleMain, 20\)/
|
||||
);
|
||||
assert.doesNotMatch(armCapture, /setInterval\(sampleMain/);
|
||||
assert.match(
|
||||
stopCapture,
|
||||
/clearInterval\(state\.sampleTimer\)[\s\S]*state\.sampleTimer = null[\s\S]*await startCapture\(nextOptions\)/
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
source.match(/\bsetInterval\s*\(/g)?.length,
|
||||
1,
|
||||
'the injected main capture must own exactly one interval timer'
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Phase duration and epoch endpoints are read from separate clocks in separate
|
||||
* calls. The enclosing request/operation window is the causal upper bound.
|
||||
*/
|
||||
export function fitsPerformanceDurationEnvelope(
|
||||
durationMs: number,
|
||||
envelopeStartEpochMs: number,
|
||||
envelopeEndEpochMs: number
|
||||
): boolean {
|
||||
return (
|
||||
Number.isFinite(durationMs) &&
|
||||
durationMs >= 0 &&
|
||||
Number.isFinite(envelopeStartEpochMs) &&
|
||||
Number.isFinite(envelopeEndEpochMs) &&
|
||||
envelopeEndEpochMs >= envelopeStartEpochMs &&
|
||||
durationMs <= envelopeEndEpochMs - envelopeStartEpochMs
|
||||
);
|
||||
}
|
||||
@@ -2,11 +2,74 @@ import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
createDiagnosticsPerformancePhaseEventParser,
|
||||
pairPerformancePhaseEvents,
|
||||
requirePerformancePhase,
|
||||
} from './performance-phase-events';
|
||||
|
||||
describe('performance phase event pairing', () => {
|
||||
it('restores and invokes the diagnostics parser without module-scoped helpers', () => {
|
||||
const restore = <T>(source: string): T =>
|
||||
new Function(`"use strict"; return (${source});`)() as T;
|
||||
const restored = restore<
|
||||
typeof createDiagnosticsPerformancePhaseEventParser
|
||||
>(createDiagnosticsPerformancePhaseEventParser.toString());
|
||||
const parse = restored({
|
||||
allowedPhases: ['xtream.network.total'],
|
||||
timelineType: 'xtream-main-phase',
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
parse({
|
||||
boundary: 'start',
|
||||
durationMs: null,
|
||||
epochMs: 100,
|
||||
phase: 'xtream.network.total',
|
||||
requestId: 'xtream-mock1-1',
|
||||
}),
|
||||
{
|
||||
boundary: 'start',
|
||||
durationMs: null,
|
||||
epochMs: 100,
|
||||
phase: 'xtream.network.total',
|
||||
requestId: 'xtream-mock1-1',
|
||||
type: 'xtream-main-phase',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the valid M3U diagnostics normalization byte-identical when subscriptions are parameterized', () => {
|
||||
const parse = createDiagnosticsPerformancePhaseEventParser({
|
||||
allowedPhases: ['data.acquire', 'parse.m3u', 'normalize'],
|
||||
timelineType: 'm3u-import-phase',
|
||||
});
|
||||
const incoming = {
|
||||
boundary: 'end',
|
||||
durationMs: 12.5,
|
||||
epochMs: 113,
|
||||
metadata: { byteCount: 2048, itemCount: 10 },
|
||||
phase: 'data.acquire',
|
||||
requestId: 'request-1',
|
||||
};
|
||||
const legacyNormalized = {
|
||||
boundary: 'end',
|
||||
byteCount: 2048,
|
||||
durationMs: 12.5,
|
||||
epochMs: 113,
|
||||
itemCount: 10,
|
||||
phase: 'data.acquire',
|
||||
requestId: 'request-1',
|
||||
type: 'm3u-import-phase',
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
JSON.stringify(parse(incoming)),
|
||||
JSON.stringify(legacyNormalized)
|
||||
);
|
||||
assert.equal(parse({ ...incoming, secret: 'not allowed' }), null);
|
||||
assert.equal(parse({ ...incoming, metadata: { itemCount: -1 } }), null);
|
||||
});
|
||||
|
||||
it('pairs request-scoped boundaries and preserves end metadata', () => {
|
||||
const phases = pairPerformancePhaseEvents([
|
||||
{
|
||||
|
||||
@@ -18,6 +18,107 @@ interface ParsedPerformancePhaseEvent {
|
||||
readonly requestId: string;
|
||||
}
|
||||
|
||||
export interface DiagnosticsPerformancePhaseEventParserOptions {
|
||||
readonly allowedPhases: readonly string[];
|
||||
readonly timelineType: string;
|
||||
}
|
||||
|
||||
export interface DiagnosticsPerformancePhaseTimelineEvent {
|
||||
readonly boundary: 'end' | 'start';
|
||||
readonly byteCount?: number;
|
||||
readonly durationMs: number | null;
|
||||
readonly epochMs: number;
|
||||
readonly itemCount?: number;
|
||||
readonly phase: string;
|
||||
readonly requestId: string;
|
||||
readonly type: string;
|
||||
}
|
||||
|
||||
export function createDiagnosticsPerformancePhaseEventParser(
|
||||
options: DiagnosticsPerformancePhaseEventParserOptions
|
||||
): (input: unknown) => DiagnosticsPerformancePhaseTimelineEvent | null {
|
||||
const allowedPhases = new Set(options.allowedPhases);
|
||||
const eventKeys = new Set([
|
||||
'boundary',
|
||||
'durationMs',
|
||||
'epochMs',
|
||||
'metadata',
|
||||
'phase',
|
||||
'requestId',
|
||||
]);
|
||||
const metadataKeys = new Set(['byteCount', 'itemCount']);
|
||||
const helpers = {
|
||||
isCount(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) >= 0;
|
||||
},
|
||||
isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return (input: unknown) => {
|
||||
if (
|
||||
!helpers.isRecord(input) ||
|
||||
Object.keys(input).some((key) => !eventKeys.has(key))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const boundary = input['boundary'];
|
||||
const durationMs = input['durationMs'];
|
||||
const epochMs = input['epochMs'];
|
||||
const metadata = input['metadata'];
|
||||
const phase = input['phase'];
|
||||
const requestId = input['requestId'];
|
||||
if (
|
||||
(boundary !== 'start' && boundary !== 'end') ||
|
||||
typeof epochMs !== 'number' ||
|
||||
!Number.isFinite(epochMs) ||
|
||||
epochMs < 0 ||
|
||||
typeof phase !== 'string' ||
|
||||
!allowedPhases.has(phase) ||
|
||||
typeof requestId !== 'string' ||
|
||||
requestId.length === 0 ||
|
||||
(boundary === 'start' && durationMs !== null) ||
|
||||
(boundary === 'end' &&
|
||||
(typeof durationMs !== 'number' ||
|
||||
!Number.isFinite(durationMs) ||
|
||||
durationMs < 0))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
metadata !== undefined &&
|
||||
(!helpers.isRecord(metadata) ||
|
||||
Object.keys(metadata).some((key) => !metadataKeys.has(key)) ||
|
||||
Object.values(metadata).some(
|
||||
(value) => !helpers.isCount(value)
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const byteCount = helpers.isRecord(metadata)
|
||||
? metadata['byteCount']
|
||||
: undefined;
|
||||
const itemCount = helpers.isRecord(metadata)
|
||||
? metadata['itemCount']
|
||||
: undefined;
|
||||
return {
|
||||
boundary,
|
||||
...(helpers.isCount(byteCount) ? { byteCount } : {}),
|
||||
durationMs: durationMs as number | null,
|
||||
epochMs,
|
||||
...(helpers.isCount(itemCount) ? { itemCount } : {}),
|
||||
phase,
|
||||
requestId,
|
||||
type: options.timelineType,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function pairPerformancePhaseEvents(
|
||||
input: readonly unknown[]
|
||||
): readonly PairedPerformancePhase[] {
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, test } from 'node:test';
|
||||
|
||||
import {
|
||||
createRendererArtifactCapture,
|
||||
startRendererDiagnosticCapture,
|
||||
stopRendererDiagnosticCapture,
|
||||
takeRendererHeapSnapshot,
|
||||
} from './m3u-import-renderer-artifacts';
|
||||
|
||||
const directories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
});
|
||||
|
||||
test('creates renderer raw profiles exclusively with owner-only permissions', async () => {
|
||||
const directory = await createDirectory();
|
||||
const session = new FakeCdpSession();
|
||||
const artifacts = createRendererArtifactCapture({
|
||||
diagnostic: true,
|
||||
outputDirectory: directory,
|
||||
});
|
||||
|
||||
await startRendererDiagnosticCapture(session as never, artifacts);
|
||||
await stopRendererDiagnosticCapture(session as never, artifacts);
|
||||
await takeRendererHeapSnapshot(
|
||||
session as never,
|
||||
artifacts.heapSnapshotPath
|
||||
);
|
||||
|
||||
for (const filename of [
|
||||
'renderer.cpuprofile',
|
||||
'renderer.heapsnapshot',
|
||||
'renderer.trace.json',
|
||||
]) {
|
||||
assert.equal(
|
||||
(await stat(join(directory, filename))).mode & 0o777,
|
||||
0o600,
|
||||
filename
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('never replaces an existing renderer CPU profile', async () => {
|
||||
const directory = await createDirectory();
|
||||
const profilePath = join(directory, 'renderer.cpuprofile');
|
||||
await writeFile(profilePath, 'original', 'utf8');
|
||||
const session = new FakeCdpSession();
|
||||
const artifacts = createRendererArtifactCapture({
|
||||
diagnostic: true,
|
||||
outputDirectory: directory,
|
||||
});
|
||||
|
||||
await startRendererDiagnosticCapture(session as never, artifacts);
|
||||
await assert.rejects(
|
||||
stopRendererDiagnosticCapture(session as never, artifacts),
|
||||
{ code: 'EEXIST' }
|
||||
);
|
||||
assert.equal(await readFile(profilePath, 'utf8'), 'original');
|
||||
});
|
||||
|
||||
test('uses exclusive owner-only writes for main and worker raw artifacts', async () => {
|
||||
const source = await readFile(
|
||||
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/fs\.writeFileSync\(\s*record\.profilePath,[\s\S]{0,220}flag: 'wx',[\s\S]{0,80}mode: 0o600/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/fs\.createWriteStream\(record\.snapshotPath,\s*\{[\s\S]{0,80}flags: 'wx',[\s\S]{0,80}mode: 0o600/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/fs\.writeFileSync\(\s*state\.mainProfilePath,[\s\S]{0,220}flag: 'wx',[\s\S]{0,80}mode: 0o600/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/fs\.createWriteStream\(\s*state\.mainSnapshotPath,\s*\{[\s\S]{0,80}flags: 'wx',[\s\S]{0,80}mode: 0o600/
|
||||
);
|
||||
});
|
||||
|
||||
class FakeCdpSession extends EventEmitter {
|
||||
async send(method: string): Promise<Record<string, unknown>> {
|
||||
if (method === 'Profiler.stop') {
|
||||
return {
|
||||
profile: {
|
||||
endTime: 2,
|
||||
nodes: [{ callFrame: {}, hitCount: 1, id: 1 }],
|
||||
samples: [1],
|
||||
startTime: 1,
|
||||
timeDeltas: [1],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === 'Tracing.end') {
|
||||
this.emit('Tracing.tracingComplete');
|
||||
}
|
||||
if (method === 'HeapProfiler.takeHeapSnapshot') {
|
||||
this.emit('HeapProfiler.addHeapSnapshotChunk', {
|
||||
chunk: '{"snapshot":{}}',
|
||||
});
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function createDirectory(): Promise<string> {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-performance-artifact-')
|
||||
);
|
||||
directories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
createPerformanceTimelineMergeApi,
|
||||
type PerformanceTimelineMergeApi,
|
||||
} from './performance-timeline-merge';
|
||||
|
||||
describe('performance timeline merge', () => {
|
||||
it('restores without module closures for Electron-main injection', () => {
|
||||
const source = createPerformanceTimelineMergeApi.toString();
|
||||
assert.doesNotMatch(source, /__name/);
|
||||
const factory = new Function(
|
||||
`"use strict"; return (${source});`
|
||||
)() as typeof createPerformanceTimelineMergeApi;
|
||||
assert.equal(factory().merge([], []).length, 0);
|
||||
});
|
||||
|
||||
it('inserts Xtream records by epoch without reordering existing M3U JSON', () => {
|
||||
const api: PerformanceTimelineMergeApi =
|
||||
createPerformanceTimelineMergeApi();
|
||||
const baseline = [
|
||||
{ epochMs: 100, type: 'm3u-a' },
|
||||
{ epochMs: 300, type: 'm3u-b' },
|
||||
{ epochMs: 300, type: 'm3u-c' },
|
||||
];
|
||||
const additions = [
|
||||
{ epochMs: 300, type: 'xtream-c' },
|
||||
{ epochMs: 50, type: 'xtream-a' },
|
||||
{ epochMs: 200, type: 'xtream-b' },
|
||||
{ epochMs: 400, type: 'xtream-d' },
|
||||
];
|
||||
|
||||
const merged = api.merge(baseline, additions);
|
||||
|
||||
assert.deepEqual(
|
||||
merged.map(({ type }) => type),
|
||||
[
|
||||
'xtream-a',
|
||||
'm3u-a',
|
||||
'xtream-b',
|
||||
'm3u-b',
|
||||
'm3u-c',
|
||||
'xtream-c',
|
||||
'xtream-d',
|
||||
]
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(merged.filter(({ type }) => type.startsWith('m3u'))),
|
||||
JSON.stringify(baseline)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
export interface PerformanceTimelineRecord {
|
||||
readonly epochMs: number;
|
||||
}
|
||||
|
||||
export interface PerformanceTimelineMergeApi {
|
||||
merge<T extends PerformanceTimelineRecord>(
|
||||
baseline: readonly T[],
|
||||
additions: readonly T[]
|
||||
): T[];
|
||||
}
|
||||
|
||||
export function createPerformanceTimelineMergeApi(): PerformanceTimelineMergeApi {
|
||||
const helpers = {
|
||||
compare(
|
||||
left: {
|
||||
readonly index: number;
|
||||
readonly record: PerformanceTimelineRecord;
|
||||
},
|
||||
right: {
|
||||
readonly index: number;
|
||||
readonly record: PerformanceTimelineRecord;
|
||||
}
|
||||
): number {
|
||||
return (
|
||||
left.record.epochMs - right.record.epochMs ||
|
||||
left.index - right.index
|
||||
);
|
||||
},
|
||||
};
|
||||
const api: PerformanceTimelineMergeApi = {
|
||||
merge<T extends PerformanceTimelineRecord>(
|
||||
baseline: readonly T[],
|
||||
additions: readonly T[]
|
||||
): T[] {
|
||||
const indexed: Array<{ index: number; record: T }> = [];
|
||||
for (let index = 0; index < additions.length; index += 1) {
|
||||
indexed.push({ index, record: additions[index] as T });
|
||||
}
|
||||
indexed.sort(helpers.compare);
|
||||
|
||||
const merged: T[] = [];
|
||||
let additionIndex = 0;
|
||||
for (const record of baseline) {
|
||||
while (
|
||||
additionIndex < indexed.length &&
|
||||
(indexed[additionIndex]?.record.epochMs ??
|
||||
Number.POSITIVE_INFINITY) < record.epochMs
|
||||
) {
|
||||
merged.push(indexed[additionIndex]?.record as T);
|
||||
additionIndex += 1;
|
||||
}
|
||||
merged.push(record);
|
||||
}
|
||||
while (additionIndex < indexed.length) {
|
||||
merged.push(indexed[additionIndex]?.record as T);
|
||||
additionIndex += 1;
|
||||
}
|
||||
return merged;
|
||||
},
|
||||
};
|
||||
return Object.freeze(api);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { parseWorkerPerformancePhaseEvents } from './worker-performance-phase-events';
|
||||
import { parseXtreamWorkerPerformancePhaseEvents } from './xtream-worker-phase-events';
|
||||
|
||||
function phasePair(
|
||||
phase: string,
|
||||
startEpochMs: number,
|
||||
durationMs: number,
|
||||
requestId = 'request-1'
|
||||
) {
|
||||
return [
|
||||
{
|
||||
boundary: 'start',
|
||||
durationMs: null,
|
||||
epochMs: startEpochMs,
|
||||
phase,
|
||||
requestId,
|
||||
},
|
||||
{
|
||||
boundary: 'end',
|
||||
durationMs,
|
||||
epochMs: startEpochMs + 1,
|
||||
metadata: { itemCount: 1 },
|
||||
phase,
|
||||
requestId,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
test('accepts producer-shaped worker output when separately read clocks have a wider monotonic duration', () => {
|
||||
const events = phasePair('sqlite.categories.read', 11, 1.6115);
|
||||
const parsed = parseXtreamWorkerPerformancePhaseEvents(
|
||||
'DB_GET_CATEGORIES',
|
||||
events,
|
||||
10,
|
||||
30,
|
||||
true
|
||||
);
|
||||
assert.deepEqual(parsed, events);
|
||||
assert.equal(Number(events[1]?.epochMs) - Number(events[0]?.epochMs), 1);
|
||||
});
|
||||
|
||||
test('rejects an Xtream worker duration outside its request envelope', () => {
|
||||
assert.equal(
|
||||
parseXtreamWorkerPerformancePhaseEvents(
|
||||
'DB_GET_CATEGORIES',
|
||||
phasePair('sqlite.categories.read', 11, 999),
|
||||
10,
|
||||
30,
|
||||
true
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects an out-of-envelope duration in a failed partial DB_SAVE_CONTENT capture', () => {
|
||||
const events = [
|
||||
...phasePair('sqlite.content.category-map-read', 11, 1),
|
||||
...phasePair('normalize.content', 13, 999),
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
parseXtreamWorkerPerformancePhaseEvents(
|
||||
'DB_SAVE_CONTENT',
|
||||
events,
|
||||
10,
|
||||
30,
|
||||
false
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts cross-clock skew but rejects an app-playlist duration outside the request envelope', () => {
|
||||
const producerShaped = [
|
||||
...phasePair('sqlite.read', 11, 1.6115),
|
||||
...phasePair('deserialize.playlist', 13, 1),
|
||||
];
|
||||
assert.deepEqual(
|
||||
parseWorkerPerformancePhaseEvents(
|
||||
'DB_GET_APP_PLAYLIST',
|
||||
producerShaped,
|
||||
10,
|
||||
30,
|
||||
true
|
||||
),
|
||||
producerShaped
|
||||
);
|
||||
|
||||
const outsideEnvelope = [
|
||||
...phasePair('sqlite.read', 11, 20.001),
|
||||
...phasePair('deserialize.playlist', 13, 1),
|
||||
];
|
||||
assert.equal(
|
||||
parseWorkerPerformancePhaseEvents(
|
||||
'DB_GET_APP_PLAYLIST',
|
||||
outsideEnvelope,
|
||||
10,
|
||||
30,
|
||||
true
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
@@ -2,9 +2,12 @@ import {
|
||||
APP_PLAYLIST_GET_PERFORMANCE_PHASE,
|
||||
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
|
||||
type AppPlaylistPerformancePhase,
|
||||
type DatabaseWorkerPerformancePhase,
|
||||
type PerformancePhaseEvent,
|
||||
type PerformancePhaseMetadata,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { fitsPerformanceDurationEnvelope } from './performance-phase-duration-envelope';
|
||||
import { parseXtreamWorkerPerformancePhaseEvents } from './xtream-worker-phase-events';
|
||||
|
||||
const PHASE_SEQUENCES = {
|
||||
DB_GET_APP_PLAYLIST: [
|
||||
@@ -31,8 +34,20 @@ export function parseWorkerPerformancePhaseEvents(
|
||||
operation: string | null,
|
||||
input: unknown,
|
||||
workStartedEpochMs: number,
|
||||
workEndedEpochMs: number
|
||||
): readonly PerformancePhaseEvent<AppPlaylistPerformancePhase>[] | null {
|
||||
workEndedEpochMs: number,
|
||||
requestSucceeded: boolean
|
||||
): readonly PerformancePhaseEvent<DatabaseWorkerPerformancePhase>[] | null {
|
||||
const xtreamEvents = parseXtreamWorkerPerformancePhaseEvents(
|
||||
operation,
|
||||
input,
|
||||
workStartedEpochMs,
|
||||
workEndedEpochMs,
|
||||
requestSucceeded
|
||||
);
|
||||
if (xtreamEvents !== undefined) {
|
||||
return xtreamEvents;
|
||||
}
|
||||
|
||||
const phaseSequence = getPhaseSequence(operation);
|
||||
if (phaseSequence.length === 0) {
|
||||
return input === undefined ||
|
||||
@@ -40,15 +55,11 @@ export function parseWorkerPerformancePhaseEvents(
|
||||
? []
|
||||
: null;
|
||||
}
|
||||
if (
|
||||
!Array.isArray(input) ||
|
||||
input.length !== phaseSequence.length * 2
|
||||
) {
|
||||
if (!Array.isArray(input) || input.length !== phaseSequence.length * 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed: PerformancePhaseEvent<AppPlaylistPerformancePhase>[] =
|
||||
[];
|
||||
const parsed: PerformancePhaseEvent<AppPlaylistPerformancePhase>[] = [];
|
||||
let lastEpochMs = workStartedEpochMs;
|
||||
for (let index = 0; index < input.length; index += 1) {
|
||||
const raw = input[index];
|
||||
@@ -81,6 +92,18 @@ export function parseWorkerPerformancePhaseEvents(
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const phaseStart = parsed[parsed.length - 1];
|
||||
if (
|
||||
boundary === 'end' &&
|
||||
(!phaseStart ||
|
||||
!fitsPerformanceDurationEnvelope(
|
||||
Number(durationMs),
|
||||
workStartedEpochMs,
|
||||
workEndedEpochMs
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
parsed.push({
|
||||
boundary: boundary as 'end' | 'start',
|
||||
|
||||
+68
-9
@@ -21,10 +21,7 @@ const validCapture = {
|
||||
workStartedEpochMs: 1_010,
|
||||
} as const;
|
||||
|
||||
function normalizeForOperation(
|
||||
operation: string,
|
||||
performanceCapture: unknown
|
||||
) {
|
||||
function normalizeForOperation(operation: string, performanceCapture: unknown) {
|
||||
return normalizeWorkerRequestPerformanceOutcome({
|
||||
ipcCallId: 2,
|
||||
operation,
|
||||
@@ -40,7 +37,7 @@ function normalizeForOperation(
|
||||
}
|
||||
|
||||
function normalize(performanceCapture: unknown) {
|
||||
return normalizeForOperation('DB_GET_CONTENT', performanceCapture);
|
||||
return normalizeForOperation('DB_HAS_CONTENT', performanceCapture);
|
||||
}
|
||||
|
||||
function assertClosedCapture(
|
||||
@@ -313,10 +310,7 @@ test('requires the exact operation-specific worker phase sequence', () => {
|
||||
);
|
||||
}
|
||||
|
||||
for (const operation of [
|
||||
'DB_GET_APP_PLAYLIST',
|
||||
'DB_UPSERT_APP_PLAYLIST',
|
||||
]) {
|
||||
for (const operation of ['DB_GET_APP_PLAYLIST', 'DB_UPSERT_APP_PLAYLIST']) {
|
||||
assertClosedCapture(
|
||||
normalizeForOperation(operation, validCapture),
|
||||
'worker-performance-capture-invalid'
|
||||
@@ -325,3 +319,68 @@ test('requires the exact operation-specific worker phase sequence', () => {
|
||||
|
||||
assert.deepEqual(normalize(validCapture).phaseEvents, []);
|
||||
});
|
||||
|
||||
test('routes finalized Xtream phase sequences before the legacy M3U parser', () => {
|
||||
const xtreamPhaseEvents = [
|
||||
{
|
||||
boundary: 'start',
|
||||
durationMs: null,
|
||||
epochMs: 1_015,
|
||||
phase: 'sqlite.content.category-map-read',
|
||||
requestId: 'request-1',
|
||||
},
|
||||
{
|
||||
boundary: 'end',
|
||||
durationMs: 2,
|
||||
epochMs: 1_017,
|
||||
metadata: { itemCount: 60 },
|
||||
phase: 'sqlite.content.category-map-read',
|
||||
requestId: 'request-1',
|
||||
},
|
||||
{
|
||||
boundary: 'start',
|
||||
durationMs: null,
|
||||
epochMs: 1_018,
|
||||
phase: 'normalize.content',
|
||||
requestId: 'request-1',
|
||||
},
|
||||
{
|
||||
boundary: 'end',
|
||||
durationMs: 5,
|
||||
epochMs: 1_023,
|
||||
metadata: { itemCount: 60_000 },
|
||||
phase: 'normalize.content',
|
||||
requestId: 'request-1',
|
||||
},
|
||||
{
|
||||
boundary: 'start',
|
||||
durationMs: null,
|
||||
epochMs: 1_024,
|
||||
phase: 'sqlite.content.write-transactions',
|
||||
requestId: 'request-1',
|
||||
},
|
||||
{
|
||||
boundary: 'end',
|
||||
durationMs: 10,
|
||||
epochMs: 1_034,
|
||||
metadata: { itemCount: 60_000 },
|
||||
phase: 'sqlite.content.write-transactions',
|
||||
requestId: 'request-1',
|
||||
},
|
||||
] as const;
|
||||
|
||||
assert.deepEqual(
|
||||
normalizeForOperation('DB_SAVE_CONTENT', {
|
||||
...validCapture,
|
||||
phaseEvents: xtreamPhaseEvents,
|
||||
}).phaseEvents,
|
||||
xtreamPhaseEvents
|
||||
);
|
||||
assertClosedCapture(
|
||||
normalizeForOperation('DB_SAVE_CONTENT', {
|
||||
...validCapture,
|
||||
phaseEvents: xtreamPhaseEvents.slice(0, -2),
|
||||
}),
|
||||
'worker-performance-capture-invalid'
|
||||
);
|
||||
});
|
||||
@@ -55,7 +55,7 @@ function requestTransport(
|
||||
) {
|
||||
return {
|
||||
ipcCallId: 7,
|
||||
operation: 'DB_GET_CONTENT',
|
||||
operation: 'DB_HAS_CONTENT',
|
||||
operationId: 'operation-42',
|
||||
operationIdUnavailableReason: null,
|
||||
performanceCapture,
|
||||
@@ -227,7 +227,7 @@ test('main capture retains raw request identity, timestamps, metrics, and reason
|
||||
assert.match(source, /ipcCallId: request\.identity\.ipcCallId/);
|
||||
assert.match(source, /identity: request\.identity/);
|
||||
assert.match(source, /performanceCapture:\s+message\['performance'\]/);
|
||||
assert.match(source, /sourceEpochMs: request\.identity\.sourceEpochMs/);
|
||||
assert.match(source, /sourceEpochMs:\s+request\.identity\.sourceEpochMs/);
|
||||
assert.match(source, /captureGeneration: state\.captureGeneration/);
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
|
||||
@@ -152,7 +152,8 @@ function parseEventLoopDelay(
|
||||
|
||||
function parseWorkerPerformanceCapture(
|
||||
input: unknown,
|
||||
operation: string | null
|
||||
operation: string | null,
|
||||
requestSucceeded: boolean
|
||||
): ParsedWorkerPerformanceCapture | null {
|
||||
if (!isRecord(input)) {
|
||||
return null;
|
||||
@@ -203,7 +204,8 @@ function parseWorkerPerformanceCapture(
|
||||
operation,
|
||||
input['phaseEvents'],
|
||||
workStartedEpochMs,
|
||||
workEndedEpochMs
|
||||
workEndedEpochMs,
|
||||
requestSucceeded
|
||||
);
|
||||
if (phaseEvents === null) {
|
||||
return null;
|
||||
@@ -318,7 +320,8 @@ export function normalizeWorkerRequestPerformanceOutcome(
|
||||
|
||||
const performanceCapture = parseWorkerPerformanceCapture(
|
||||
input.performanceCapture,
|
||||
input.operation
|
||||
input.operation,
|
||||
input.success
|
||||
);
|
||||
if (!performanceCapture) {
|
||||
return unavailable('worker-performance-capture-invalid');
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { attributeXtreamPhases } from './xtream-phase-attribution';
|
||||
import { scenarioCapture } from './xtream-phase-attribution.fixtures';
|
||||
import {
|
||||
expectedXtreamIpc,
|
||||
expectedXtreamPhases,
|
||||
XTREAM_ATTRIBUTION_PHASE as PHASE,
|
||||
XTREAM_PHASE_SEMANTIC_TYPE as SEMANTIC,
|
||||
} from './xtream-phase-inventory';
|
||||
|
||||
const EXPECTED_PROVIDER_CALLS: Readonly<Record<XtreamScenarioId, number>> = {
|
||||
[XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE]: 8,
|
||||
[XTREAM_SCENARIO_ID.REFRESH_LARGE]: 8,
|
||||
[XTREAM_SCENARIO_ID.DELETE_LARGE]: 0,
|
||||
[XTREAM_SCENARIO_ID.CANCEL_IMPORT]: 6,
|
||||
[XTREAM_SCENARIO_ID.BACKGROUND_UI]: 8,
|
||||
};
|
||||
|
||||
const EXPECTED_ACCOUNT_CALLS: Readonly<Record<XtreamScenarioId, number>> = {
|
||||
[XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE]: 2,
|
||||
[XTREAM_SCENARIO_ID.REFRESH_LARGE]: 2,
|
||||
[XTREAM_SCENARIO_ID.DELETE_LARGE]: 0,
|
||||
[XTREAM_SCENARIO_ID.CANCEL_IMPORT]: 2,
|
||||
[XTREAM_SCENARIO_ID.BACKGROUND_UI]: 2,
|
||||
};
|
||||
|
||||
describe('Xtream account phase inventory', () => {
|
||||
it('counts every scenario-specific provider IPC and phase lifecycle', () => {
|
||||
for (const scenarioId of Object.values(XTREAM_SCENARIO_ID)) {
|
||||
const providerCalls = EXPECTED_PROVIDER_CALLS[scenarioId];
|
||||
assert.equal(
|
||||
expectedXtreamPhases(scenarioId).get(PHASE.NETWORK_TOTAL)
|
||||
?.occurrences,
|
||||
providerCalls
|
||||
);
|
||||
assert.equal(
|
||||
expectedXtreamIpc(scenarioId).get('xtreamRequest')?.success ??
|
||||
0,
|
||||
providerCalls
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('fixtures contain the exact account lifecycle count and remain valid', () => {
|
||||
for (const scenarioId of Object.values(XTREAM_SCENARIO_ID)) {
|
||||
const capture = scenarioCapture(scenarioId);
|
||||
const accounts = capture.events.filter(
|
||||
({ boundary, phase, semanticType }) =>
|
||||
boundary === 'start' &&
|
||||
phase === PHASE.NETWORK_TOTAL &&
|
||||
semanticType === SEMANTIC.ACCOUNT
|
||||
);
|
||||
assert.equal(accounts.length, EXPECTED_ACCOUNT_CALLS[scenarioId]);
|
||||
assert.doesNotThrow(() => attributeXtreamPhases(capture));
|
||||
}
|
||||
});
|
||||
|
||||
it('allows the two independent fresh-add account phases to overlap', () => {
|
||||
const capture = scenarioCapture(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
);
|
||||
const events = capture.events.map((event) =>
|
||||
event.correlationId === 'provider-account-route'
|
||||
? { ...event, epochMs: event.epochMs - 6.5 }
|
||||
: event
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
attributeXtreamPhases({ ...capture, events })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
XTREAM_MAIN_PERFORMANCE_PHASE,
|
||||
XTREAM_PRELOAD_PERFORMANCE_METHOD,
|
||||
XtreamCodeActions,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
import {
|
||||
createXtreamIpcMarkerCapture,
|
||||
type XtreamIpcMarkerCapture,
|
||||
type XtreamIpcMarkerCaptureOptions,
|
||||
} from './xtream-ipc-marker-events';
|
||||
|
||||
const OPTIONS: XtreamIpcMarkerCaptureOptions = {
|
||||
actions: Object.values(XtreamCodeActions),
|
||||
mainPhases: Object.values(XTREAM_MAIN_PERFORMANCE_PHASE),
|
||||
methods: Object.values(XTREAM_PRELOAD_PERFORMANCE_METHOD),
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
describe('Xtream account phase correlation', () => {
|
||||
it('accepts overlapping null-session calls and links main phases FIFO', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(OPTIONS);
|
||||
capture.start(42, 90);
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 100, accountMarker(1, 99)),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 102, accountMarker(2, 101)),
|
||||
true
|
||||
);
|
||||
acceptAccountMainLifecycle(capture, 1, 103);
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 113, accountMarker(1, 112, 'end')),
|
||||
true
|
||||
);
|
||||
acceptAccountMainLifecycle(capture, 2, 114);
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 124, accountMarker(2, 123, 'end')),
|
||||
true
|
||||
);
|
||||
|
||||
const snapshot = capture.stop();
|
||||
assert.deepEqual(snapshot.invalidReasons, []);
|
||||
assert.equal(snapshot.quarantinedKeyCount, 0);
|
||||
const phases = snapshot.timeline.filter(
|
||||
({ type }) => type === 'xtream-main-phase'
|
||||
);
|
||||
assert.deepEqual(
|
||||
phases.map(({ action, ipcCallId }) => ({ action, ipcCallId })),
|
||||
[
|
||||
...Array.from({ length: 6 }, () => ({
|
||||
action: XtreamCodeActions.GetAccountInfo,
|
||||
ipcCallId: 1,
|
||||
})),
|
||||
...Array.from({ length: 6 }, () => ({
|
||||
action: XtreamCodeActions.GetAccountInfo,
|
||||
ipcCallId: 2,
|
||||
})),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('fails closed for duplicate and malformed preload call IDs', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(OPTIONS);
|
||||
capture.start(42, 90);
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 100, accountMarker(1, 99)),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 102, accountMarker(1, 101)),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 104, accountMarker(0, 103)),
|
||||
false
|
||||
);
|
||||
|
||||
const snapshot = capture.stop();
|
||||
assert.equal(snapshot.quarantinedKeyCount, 1);
|
||||
assert.ok(
|
||||
snapshot.invalidReasons.includes('preload-marker-duplicate-key')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function acceptAccountMainLifecycle(
|
||||
capture: XtreamIpcMarkerCapture,
|
||||
ipcCallId: number,
|
||||
offset: number
|
||||
): void {
|
||||
const requestId = `xtream-account-${ipcCallId}`;
|
||||
acceptMainBoundary(
|
||||
capture,
|
||||
requestId,
|
||||
'xtream.network.total',
|
||||
offset + 1,
|
||||
'start'
|
||||
);
|
||||
acceptMainPair(
|
||||
capture,
|
||||
requestId,
|
||||
'xtream.json.transform',
|
||||
offset + 2,
|
||||
offset + 3
|
||||
);
|
||||
acceptMainBoundary(
|
||||
capture,
|
||||
requestId,
|
||||
'xtream.network.total',
|
||||
offset + 6,
|
||||
'end',
|
||||
5
|
||||
);
|
||||
acceptMainPair(
|
||||
capture,
|
||||
requestId,
|
||||
'xtream.response.ready',
|
||||
offset + 7,
|
||||
offset + 8
|
||||
);
|
||||
}
|
||||
|
||||
function acceptMainBoundary(
|
||||
capture: XtreamIpcMarkerCapture,
|
||||
requestId: string,
|
||||
phase: string,
|
||||
epochMs: number,
|
||||
boundary: 'end' | 'start',
|
||||
durationMs: number | null = null
|
||||
): void {
|
||||
assert.equal(
|
||||
capture.acceptMainPhase({
|
||||
boundary,
|
||||
durationMs,
|
||||
epochMs,
|
||||
phase,
|
||||
requestId,
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function acceptMainPair(
|
||||
capture: XtreamIpcMarkerCapture,
|
||||
requestId: string,
|
||||
phase: string,
|
||||
startEpochMs: number,
|
||||
endEpochMs: number
|
||||
): void {
|
||||
assert.equal(
|
||||
capture.acceptMainPhase({
|
||||
boundary: 'start',
|
||||
durationMs: null,
|
||||
epochMs: startEpochMs,
|
||||
phase,
|
||||
requestId,
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptMainPhase({
|
||||
boundary: 'end',
|
||||
durationMs: endEpochMs - startEpochMs,
|
||||
epochMs: endEpochMs,
|
||||
phase,
|
||||
requestId,
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function accountMarker(
|
||||
ipcCallId: number,
|
||||
sourceEpochMs: number,
|
||||
boundary: 'end' | 'start' = 'start'
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
sourceEpochMs,
|
||||
ipcCallId,
|
||||
method: 'xtreamRequest',
|
||||
boundary,
|
||||
outcome: boundary === 'start' ? null : 'success',
|
||||
playlistId: null,
|
||||
operationId: null,
|
||||
sessionId: null,
|
||||
action: XtreamCodeActions.GetAccountInfo,
|
||||
categoryType: null,
|
||||
contentType: null,
|
||||
itemCount: null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function deriveXtreamAcquisitionDurationMs(
|
||||
networkTotalDurationMs: number,
|
||||
jsonTransformDurationMs: number
|
||||
): number {
|
||||
if (
|
||||
!Number.isFinite(networkTotalDurationMs) ||
|
||||
networkTotalDurationMs < 0 ||
|
||||
!Number.isFinite(jsonTransformDurationMs) ||
|
||||
jsonTransformDurationMs < 0 ||
|
||||
jsonTransformDurationMs > networkTotalDurationMs
|
||||
) {
|
||||
throw new Error('xtream-acquisition-duration-invalid');
|
||||
}
|
||||
return networkTotalDurationMs - jsonTransformDurationMs;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
serializeXtreamArtifactForPersistence,
|
||||
type XtreamDiagnosticArtifactEvidence,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import { writeXtreamExclusiveArtifact } from './xtream-exclusive-artifact-writer';
|
||||
import { persistThenValidateXtreamIteration } from './xtream-benchmark-report';
|
||||
import { XTREAM_SYNTHETIC_CREDENTIALS } from './xtream-scenario-driver';
|
||||
import type {
|
||||
XtreamArtifactSafetyContext,
|
||||
XtreamRawIterationResult,
|
||||
XtreamValidatedIterationResult,
|
||||
} from './xtream-summary-contract';
|
||||
|
||||
export interface PersistedXtreamIteration {
|
||||
readonly diagnosticEvidence: XtreamDiagnosticArtifactEvidence | null;
|
||||
readonly iteration: XtreamValidatedIterationResult;
|
||||
}
|
||||
|
||||
export function createXtreamArtifactSafetyContext(
|
||||
controlToken: string
|
||||
): XtreamArtifactSafetyContext {
|
||||
return Object.freeze({
|
||||
controlToken,
|
||||
syntheticCredentials: XTREAM_SYNTHETIC_CREDENTIALS,
|
||||
});
|
||||
}
|
||||
|
||||
export async function writeXtreamSafeJson(
|
||||
absolutePath: string,
|
||||
value: unknown,
|
||||
safety: XtreamArtifactSafetyContext
|
||||
): Promise<void> {
|
||||
const serialized = serializeXtreamArtifactForPersistence(value, safety);
|
||||
await writeXtreamExclusiveArtifact(absolutePath, serialized);
|
||||
}
|
||||
|
||||
export async function persistXtreamIteration(
|
||||
iterationDirectory: string,
|
||||
raw: XtreamRawIterationResult,
|
||||
diagnosticEvidence: XtreamDiagnosticArtifactEvidence | null,
|
||||
safety: XtreamArtifactSafetyContext
|
||||
): Promise<PersistedXtreamIteration> {
|
||||
const iteration = await persistThenValidateXtreamIteration(
|
||||
raw,
|
||||
safety,
|
||||
(serialized) =>
|
||||
writeXtreamExclusiveArtifact(
|
||||
join(iterationDirectory, 'result.json'),
|
||||
serialized
|
||||
)
|
||||
);
|
||||
return Object.freeze({ diagnosticEvidence, iteration });
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
|
||||
import { workspaceRoot } from '../electron-test-fixtures';
|
||||
import { createXtreamIterationDefinitions } from './xtream-benchmark-contract';
|
||||
import { BUILD_IDENTITY } from './xtream-benchmark-report.fixtures';
|
||||
import {
|
||||
type XtreamBenchmarkConfiguration,
|
||||
assertXtreamBenchmarkBuildUnchanged,
|
||||
assertXtreamBenchmarkOutputPathHasNoSymlinks,
|
||||
createXtreamBenchmarkManifest,
|
||||
resolveXtreamBenchmarkCdpPort,
|
||||
resolveXtreamBenchmarkConfiguration,
|
||||
resolveXtreamBenchmarkOutputLayout,
|
||||
XTREAM_BENCHMARK_MOCK_ORIGIN,
|
||||
} from './xtream-benchmark-configuration';
|
||||
import { XTREAM_MEMORY_ACCOUNTING } from './xtream-summary-contract';
|
||||
|
||||
const SOURCE = Object.freeze({
|
||||
gitCommit: '0123456789abcdef0123456789abcdef01234567',
|
||||
gitDiffSha256:
|
||||
'abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd',
|
||||
gitDirty: false,
|
||||
});
|
||||
const FIXTURE = Object.freeze({
|
||||
bytes: 12_345,
|
||||
catalogSha256:
|
||||
'1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
||||
counts: {
|
||||
categories: { live: 60, vod: 20, series: 20, total: 100 },
|
||||
items: {
|
||||
live: 60_000,
|
||||
vod: 20_000,
|
||||
series: 20_000,
|
||||
total: 100_000,
|
||||
},
|
||||
},
|
||||
epoch: 1,
|
||||
scenario: 'performance-100k',
|
||||
seed: 91_001,
|
||||
} as const);
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
});
|
||||
|
||||
describe('Xtream benchmark output configuration', () => {
|
||||
it('accepts only a fresh timestamp directory directly below dist/performance', async () => {
|
||||
const repositoryRoot = await createRepositoryRoot();
|
||||
const performanceRoot = join(repositoryRoot, 'dist', 'performance');
|
||||
const outputRoot = join(
|
||||
performanceRoot,
|
||||
'20260728T123456Z-xtream-smoke'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
resolveXtreamBenchmarkOutputLayout(
|
||||
repositoryRoot,
|
||||
outputRoot,
|
||||
'baseline'
|
||||
),
|
||||
{
|
||||
outputDirectory: join(outputRoot, 'baseline'),
|
||||
outputRoot,
|
||||
performanceRoot,
|
||||
}
|
||||
);
|
||||
|
||||
for (const requestedRoot of [
|
||||
'dist/performance/20260728T123456Z',
|
||||
performanceRoot,
|
||||
join(performanceRoot, 'nested', '20260728T123456Z'),
|
||||
join(performanceRoot, 'not-a-timestamp'),
|
||||
join(repositoryRoot, 'outside', '20260728T123456Z'),
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveXtreamBenchmarkOutputLayout(
|
||||
repositoryRoot,
|
||||
requestedRoot,
|
||||
'baseline'
|
||||
),
|
||||
/absolute fresh timestamp path below dist\/performance/
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() =>
|
||||
resolveXtreamBenchmarkOutputLayout(
|
||||
repositoryRoot,
|
||||
outputRoot,
|
||||
'../after'
|
||||
),
|
||||
/safe Xtream variant/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects every existing symbolic-link component', async () => {
|
||||
const repositoryRoot = await createRepositoryRoot();
|
||||
const realDist = join(repositoryRoot, 'real-dist');
|
||||
await mkdir(realDist);
|
||||
await import('node:fs/promises').then(({ symlink }) =>
|
||||
symlink(realDist, join(repositoryRoot, 'dist'))
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
assertXtreamBenchmarkOutputPathHasNoSymlinks(
|
||||
repositoryRoot,
|
||||
join(
|
||||
repositoryRoot,
|
||||
'dist',
|
||||
'performance',
|
||||
'20260728T123456Z'
|
||||
)
|
||||
),
|
||||
/contains a symbolic link/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Xtream benchmark mode configuration', () => {
|
||||
it('pins formal CDP to 9222 and bounds smoke-only alternate ports', () => {
|
||||
assert.equal(resolveXtreamBenchmarkCdpPort('formal', undefined), 9222);
|
||||
assert.equal(resolveXtreamBenchmarkCdpPort('smoke', '9322'), 9322);
|
||||
assert.throws(
|
||||
() => resolveXtreamBenchmarkCdpPort('formal', '9322'),
|
||||
/Formal Xtream performance runs require CDP port 9222/
|
||||
);
|
||||
for (const requested of ['1023', '65536', '9322.5', '0x246a']) {
|
||||
assert.throws(
|
||||
() => resolveXtreamBenchmarkCdpPort('smoke', requested),
|
||||
/IPTVNATOR_PERF_CDP_PORT is invalid/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('creates output only after preflight and uses exclusive child mkdir calls', async () => {
|
||||
const repositoryRoot = await createRepositoryRoot();
|
||||
const performanceRoot = join(repositoryRoot, 'dist', 'performance');
|
||||
await mkdir(performanceRoot, { recursive: true });
|
||||
const outputRoot = join(
|
||||
performanceRoot,
|
||||
'20260728T123456Z-xtream-smoke'
|
||||
);
|
||||
const calls: string[] = [];
|
||||
const configuration = await resolveXtreamBenchmarkConfiguration({
|
||||
dependencies: {
|
||||
assertCapacity: async () => {
|
||||
calls.push('preflight');
|
||||
return {
|
||||
availableBytes: BigInt(3 * 1_024 ** 3),
|
||||
checkedPath: performanceRoot,
|
||||
minimumBytes: BigInt(2 * 1_024 ** 3),
|
||||
};
|
||||
},
|
||||
captureBuildIdentity: async () => BUILD_IDENTITY,
|
||||
mkdir: async (path, options) => {
|
||||
calls.push(
|
||||
`mkdir:${path}:${String(
|
||||
options?.recursive ?? false
|
||||
)}:${String(options?.mode ?? 'default')}`
|
||||
);
|
||||
await mkdir(path, options);
|
||||
},
|
||||
readElectronVersion: async () => '41.0.0',
|
||||
readGitSourceState: () => SOURCE,
|
||||
},
|
||||
environment: {
|
||||
IPTVNATOR_PERF_CDP_PORT: '9322',
|
||||
IPTVNATOR_PERF_OUTPUT_DIR: outputRoot,
|
||||
IPTVNATOR_PERF_SMOKE: '1',
|
||||
IPTVNATOR_PERF_VARIANT: 'smoke',
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN: 'a'.repeat(64),
|
||||
},
|
||||
workspaceRoot: repositoryRoot,
|
||||
});
|
||||
|
||||
assert.equal(calls[0], 'preflight');
|
||||
assert.deepEqual(calls.slice(1), [
|
||||
`mkdir:${performanceRoot}:true:${0o700}`,
|
||||
`mkdir:${outputRoot}:false:${0o700}`,
|
||||
`mkdir:${join(outputRoot, 'smoke')}:false:${0o700}`,
|
||||
]);
|
||||
assert.equal((await stat(outputRoot)).mode & 0o777, 0o700);
|
||||
assert.equal(
|
||||
(await stat(join(outputRoot, 'smoke'))).mode & 0o777,
|
||||
0o700
|
||||
);
|
||||
assert.equal(configuration.mode, 'smoke');
|
||||
assert.equal(configuration.measuredRuns, 1);
|
||||
assert.equal(configuration.outputDirectory, join(outputRoot, 'smoke'));
|
||||
assert.equal(configuration.mockOrigin, XTREAM_BENCHMARK_MOCK_ORIGIN);
|
||||
assert.equal(
|
||||
createXtreamIterationDefinitions(configuration.smoke)
|
||||
.filter((entry) => entry.kind === 'measured')
|
||||
.every((entry) => !entry.eligibleForComparison),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('fails formal resolution before preflight when tracked source is dirty', async () => {
|
||||
const repositoryRoot = await createRepositoryRoot();
|
||||
let preflightCalled = false;
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
resolveXtreamBenchmarkConfiguration({
|
||||
dependencies: {
|
||||
assertCapacity: async () => {
|
||||
preflightCalled = true;
|
||||
throw new Error('unexpected preflight');
|
||||
},
|
||||
captureBuildIdentity: async () => BUILD_IDENTITY,
|
||||
readElectronVersion: async () => '41.0.0',
|
||||
readGitSourceState: () => ({
|
||||
...SOURCE,
|
||||
gitDirty: true,
|
||||
}),
|
||||
},
|
||||
environment: {
|
||||
IPTVNATOR_PERF_OUTPUT_DIR: join(
|
||||
repositoryRoot,
|
||||
'dist',
|
||||
'performance',
|
||||
'20260728T123456Z-xtream'
|
||||
),
|
||||
IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN: 'b'.repeat(64),
|
||||
},
|
||||
workspaceRoot: repositoryRoot,
|
||||
}),
|
||||
/Formal Xtream performance runs require a clean Git worktree/
|
||||
);
|
||||
assert.equal(preflightCalled, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Xtream benchmark manifest provenance', () => {
|
||||
it('creates an exact manifest from the canonical prepared mock fixture only', () => {
|
||||
const configuration = configurationFixture();
|
||||
const manifest = createXtreamBenchmarkManifest(configuration, FIXTURE);
|
||||
|
||||
assert.equal(manifest.fixture.categories, 100);
|
||||
assert.equal(manifest.fixture.items, 100_000);
|
||||
assert.deepEqual(manifest.build, BUILD_IDENTITY);
|
||||
assert.deepEqual(manifest.source, SOURCE);
|
||||
assert.deepEqual(manifest.memoryAccounting, XTREAM_MEMORY_ACCOUNTING);
|
||||
assert.equal(
|
||||
JSON.stringify(manifest).includes(configuration.controlToken),
|
||||
false
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkManifest(configuration, {
|
||||
...FIXTURE,
|
||||
scenario: 'large',
|
||||
}),
|
||||
/invalid Xtream performance fixture/
|
||||
);
|
||||
});
|
||||
|
||||
it('recaptures and requires byte-identical build identity at suite end', async () => {
|
||||
const configuration = configurationFixture();
|
||||
await assert.doesNotReject(() =>
|
||||
assertXtreamBenchmarkBuildUnchanged(configuration, async () =>
|
||||
structuredClone(BUILD_IDENTITY)
|
||||
)
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
assertXtreamBenchmarkBuildUnchanged(
|
||||
configuration,
|
||||
async () => ({
|
||||
...BUILD_IDENTITY,
|
||||
renderer: {
|
||||
...BUILD_IDENTITY.renderer,
|
||||
aggregateSha256: 'f'.repeat(64),
|
||||
},
|
||||
})
|
||||
),
|
||||
/Xtream performance build changed during the suite/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Xtream Playwright and Nx plumbing', () => {
|
||||
it('uses one isolated control-enabled mock and one no-retry 30-minute test', async () => {
|
||||
const configPath = join(
|
||||
workspaceRoot,
|
||||
'apps/electron-backend-e2e/playwright.xtream-performance.config.ts'
|
||||
);
|
||||
const source = await readFile(configPath, 'utf8');
|
||||
|
||||
assert.match(source, /randomBytes\(32\)/);
|
||||
assert.match(
|
||||
source,
|
||||
/process\.env\['IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN'\]\s*\?\?/
|
||||
);
|
||||
assert.match(source, /http:\/\/127\.0\.0\.1:3221\/health/);
|
||||
assert.match(source, /reuseExistingServer:\s*false/);
|
||||
assert.match(source, /IPTVNATOR_XTREAM_MOCK_CONTROL:\s*'1'/);
|
||||
assert.match(
|
||||
source,
|
||||
/IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN:\s*controlToken/
|
||||
);
|
||||
assert.match(source, /HOST:\s*'127\.0\.0\.1'/);
|
||||
assert.match(source, /PORT:\s*'3221'/);
|
||||
assert.match(source, /retries:\s*0/);
|
||||
assert.match(source, /workers:\s*1/);
|
||||
assert.match(source, /timeout:\s*30\s*\*\s*60\s*\*\s*1_000/);
|
||||
});
|
||||
|
||||
it('pins the Nx target and keeps the entrypoint to one orchestrator call', async () => {
|
||||
const [project, entrypoint] = await Promise.all([
|
||||
readFile(
|
||||
join(workspaceRoot, 'apps/electron-backend-e2e/project.json'),
|
||||
'utf8'
|
||||
).then(
|
||||
(value) =>
|
||||
JSON.parse(value) as {
|
||||
targets: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
),
|
||||
readFile(
|
||||
join(
|
||||
workspaceRoot,
|
||||
'apps/electron-backend-e2e/src/xtream.performance.ts'
|
||||
),
|
||||
'utf8'
|
||||
),
|
||||
]);
|
||||
const target = project.targets['benchmark-xtream'];
|
||||
|
||||
assert.deepEqual(target?.['dependsOn'], [
|
||||
'electron-backend:build-performance',
|
||||
]);
|
||||
assert.equal(target?.['cache'], false);
|
||||
assert.equal(target?.['parallelism'], false);
|
||||
assert.equal(
|
||||
(target?.['options'] as Record<string, unknown>)?.['command'],
|
||||
'pnpm exec playwright test --config=playwright.xtream-performance.config.ts src/xtream.performance.ts'
|
||||
);
|
||||
assert.equal(
|
||||
(entrypoint.match(/runXtreamBenchmark\(\)/g) ?? []).length,
|
||||
1
|
||||
);
|
||||
assert.equal((entrypoint.match(/\btest\(/g) ?? []).length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
async function createRepositoryRoot(): Promise<string> {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'iptvnator-xtream-config-'));
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
function configurationFixture(): XtreamBenchmarkConfiguration {
|
||||
return {
|
||||
build: BUILD_IDENTITY,
|
||||
cdpPort: 9222,
|
||||
controlToken: 'c'.repeat(64),
|
||||
electronVersion: '41.0.0',
|
||||
measuredRuns: 5,
|
||||
mockOrigin: XTREAM_BENCHMARK_MOCK_ORIGIN,
|
||||
mode: 'formal' as const,
|
||||
outputDirectory: resolve('/tmp/xtream/baseline'),
|
||||
outputRoot: resolve('/tmp/xtream'),
|
||||
runtime: {
|
||||
arch: 'arm64',
|
||||
electronVersion: '41.0.0',
|
||||
harnessNodeVersion: '22.0.0',
|
||||
platform: 'darwin',
|
||||
},
|
||||
smoke: false,
|
||||
source: SOURCE,
|
||||
variant: 'baseline',
|
||||
workspaceRoot: resolve('/repository'),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
import { mkdir, readFile } from 'node:fs/promises';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
import { workspaceRoot as defaultWorkspaceRoot } from '../electron-test-fixtures';
|
||||
import type {
|
||||
XtreamBenchmarkManifest,
|
||||
XtreamBenchmarkRuntime,
|
||||
XtreamBenchmarkSource,
|
||||
XtreamPerformanceManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { parseXtreamBenchmarkManifest } from './xtream-benchmark-manifest-schema';
|
||||
import {
|
||||
captureXtreamBuildIdentity,
|
||||
type XtreamBenchmarkBuildIdentity,
|
||||
} from './xtream-build-identity';
|
||||
import { parseXtreamBenchmarkBuildIdentity } from './xtream-build-identity-schema';
|
||||
import { parseXtreamManifest } from './xtream-control-state-schema';
|
||||
import {
|
||||
assertXtreamBenchmarkOutputPathHasNoSymlinks,
|
||||
createExclusiveXtreamBenchmarkOutputLayout,
|
||||
resolveXtreamBenchmarkCdpPort,
|
||||
resolveXtreamBenchmarkOutputLayout,
|
||||
type XtreamMkdir,
|
||||
} from './xtream-benchmark-output-configuration';
|
||||
import {
|
||||
assertPerformanceArtifactCapacity,
|
||||
type PerformanceArtifactCapacity,
|
||||
} from './performance-artifact-preflight';
|
||||
import { XTREAM_MEMORY_ACCOUNTING } from './xtream-summary-contract';
|
||||
|
||||
export const XTREAM_BENCHMARK_MOCK_ORIGIN = 'http://127.0.0.1:3221';
|
||||
const CONTROL_TOKEN = /^[A-Za-z0-9_-]{43,128}$/u;
|
||||
|
||||
export {
|
||||
assertXtreamBenchmarkOutputPathHasNoSymlinks,
|
||||
resolveXtreamBenchmarkCdpPort,
|
||||
resolveXtreamBenchmarkOutputLayout,
|
||||
XTREAM_BENCHMARK_FORMAL_CDP_PORT,
|
||||
} from './xtream-benchmark-output-configuration';
|
||||
|
||||
export interface XtreamBenchmarkConfiguration {
|
||||
readonly build: XtreamBenchmarkBuildIdentity;
|
||||
readonly cdpPort: number;
|
||||
/** Control-plane secret. Never serialize or log this field. */
|
||||
readonly controlToken: string;
|
||||
readonly electronVersion: string;
|
||||
readonly measuredRuns: 1 | 5;
|
||||
readonly mockOrigin: typeof XTREAM_BENCHMARK_MOCK_ORIGIN;
|
||||
readonly mode: 'formal' | 'smoke';
|
||||
readonly outputDirectory: string;
|
||||
readonly outputRoot: string;
|
||||
readonly runtime: XtreamBenchmarkRuntime;
|
||||
readonly smoke: boolean;
|
||||
readonly source: XtreamBenchmarkSource;
|
||||
readonly variant: string;
|
||||
readonly workspaceRoot: string;
|
||||
}
|
||||
|
||||
type XtreamGitSourceState = XtreamBenchmarkSource;
|
||||
|
||||
interface XtreamConfigurationDependencies {
|
||||
readonly assertCapacity?: (
|
||||
targetPath: string
|
||||
) => Promise<PerformanceArtifactCapacity>;
|
||||
readonly captureBuildIdentity?: (
|
||||
workspaceRoot: string
|
||||
) => Promise<XtreamBenchmarkBuildIdentity>;
|
||||
readonly mkdir?: XtreamMkdir;
|
||||
readonly readElectronVersion?: (workspaceRoot: string) => Promise<string>;
|
||||
readonly readGitSourceState?: (
|
||||
workspaceRoot: string
|
||||
) => XtreamGitSourceState;
|
||||
}
|
||||
|
||||
export interface ResolveXtreamBenchmarkConfigurationOptions {
|
||||
readonly dependencies?: XtreamConfigurationDependencies;
|
||||
readonly environment?: NodeJS.ProcessEnv;
|
||||
readonly workspaceRoot?: string;
|
||||
}
|
||||
|
||||
export async function resolveXtreamBenchmarkConfiguration(
|
||||
options: ResolveXtreamBenchmarkConfigurationOptions = {}
|
||||
): Promise<XtreamBenchmarkConfiguration> {
|
||||
const environment = options.environment ?? process.env;
|
||||
const repositoryRoot = resolve(
|
||||
options.workspaceRoot ?? defaultWorkspaceRoot
|
||||
);
|
||||
const requestedRoot = environment['IPTVNATOR_PERF_OUTPUT_DIR'];
|
||||
if (!requestedRoot) {
|
||||
throw new Error('IPTVNATOR_PERF_OUTPUT_DIR is required');
|
||||
}
|
||||
const smoke = resolveSmokeMode(environment['IPTVNATOR_PERF_SMOKE']);
|
||||
const mode = smoke ? 'smoke' : 'formal';
|
||||
const variant = environment['IPTVNATOR_PERF_VARIANT'] ?? 'baseline';
|
||||
const layout = resolveXtreamBenchmarkOutputLayout(
|
||||
repositoryRoot,
|
||||
requestedRoot,
|
||||
variant
|
||||
);
|
||||
const cdpPort = resolveXtreamBenchmarkCdpPort(
|
||||
mode,
|
||||
environment['IPTVNATOR_PERF_CDP_PORT']
|
||||
);
|
||||
const controlToken =
|
||||
environment['IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN'] ?? '';
|
||||
if (!CONTROL_TOKEN.test(controlToken)) {
|
||||
throw new Error(
|
||||
'Xtream benchmark requires a high-entropy mock control token'
|
||||
);
|
||||
}
|
||||
|
||||
const dependencies = options.dependencies ?? {};
|
||||
const readSource = dependencies.readGitSourceState ?? readGitSourceState;
|
||||
const source = Object.freeze(readSource(repositoryRoot));
|
||||
assertSourceState(mode, source);
|
||||
const captureBuild =
|
||||
dependencies.captureBuildIdentity ?? captureXtreamBuildIdentity;
|
||||
const build = parseXtreamBenchmarkBuildIdentity(
|
||||
await captureBuild(repositoryRoot)
|
||||
);
|
||||
const electronVersion = await (
|
||||
dependencies.readElectronVersion ?? readElectronRuntimeVersion
|
||||
)(repositoryRoot);
|
||||
if (!electronVersion.trim()) {
|
||||
throw new Error('Unable to resolve the Electron runtime version');
|
||||
}
|
||||
|
||||
await assertXtreamBenchmarkOutputPathHasNoSymlinks(
|
||||
repositoryRoot,
|
||||
layout.outputRoot
|
||||
);
|
||||
await (dependencies.assertCapacity ?? assertPerformanceArtifactCapacity)(
|
||||
layout.outputDirectory
|
||||
);
|
||||
await createExclusiveXtreamBenchmarkOutputLayout(
|
||||
repositoryRoot,
|
||||
layout,
|
||||
dependencies.mkdir ?? mkdir
|
||||
);
|
||||
|
||||
const runtime = Object.freeze({
|
||||
arch: process.arch,
|
||||
electronVersion,
|
||||
harnessNodeVersion: process.versions.node,
|
||||
platform: process.platform,
|
||||
});
|
||||
return Object.freeze({
|
||||
build,
|
||||
cdpPort,
|
||||
controlToken,
|
||||
electronVersion,
|
||||
measuredRuns: smoke ? 1 : 5,
|
||||
mockOrigin: XTREAM_BENCHMARK_MOCK_ORIGIN,
|
||||
mode,
|
||||
outputDirectory: layout.outputDirectory,
|
||||
outputRoot: layout.outputRoot,
|
||||
runtime,
|
||||
smoke,
|
||||
source,
|
||||
variant,
|
||||
workspaceRoot: repositoryRoot,
|
||||
});
|
||||
}
|
||||
|
||||
export function createXtreamBenchmarkManifest(
|
||||
configuration: XtreamBenchmarkConfiguration,
|
||||
preparedFixture: unknown
|
||||
): XtreamBenchmarkManifest {
|
||||
let fixture: XtreamPerformanceManifest;
|
||||
try {
|
||||
fixture = parseXtreamManifest(preparedFixture);
|
||||
} catch {
|
||||
throw new Error('invalid Xtream performance fixture');
|
||||
}
|
||||
return parseXtreamBenchmarkManifest({
|
||||
build: configuration.build,
|
||||
environment: {
|
||||
cdpAddress: '127.0.0.1',
|
||||
cdpPort: configuration.cdpPort,
|
||||
databaseWorkerBatchDelayMs: 0,
|
||||
exposeGc: true,
|
||||
freshUserDataPerIteration: true,
|
||||
perfCapture: '1',
|
||||
perfWorkerProfiling: '1',
|
||||
traceRendererConsole: '0',
|
||||
},
|
||||
fixture: {
|
||||
bytes: fixture.bytes,
|
||||
catalogSha256: fixture.catalogSha256,
|
||||
categories: fixture.counts.categories.total,
|
||||
items: fixture.counts.items.total,
|
||||
scenario: fixture.scenario,
|
||||
seed: fixture.seed,
|
||||
},
|
||||
memoryAccounting: XTREAM_MEMORY_ACCOUNTING,
|
||||
mode: configuration.mode,
|
||||
runtime: configuration.runtime,
|
||||
source: configuration.source,
|
||||
suite: 'xtream-performance-100k',
|
||||
variant: configuration.variant,
|
||||
});
|
||||
}
|
||||
|
||||
export async function assertXtreamBenchmarkBuildUnchanged(
|
||||
configuration: XtreamBenchmarkConfiguration,
|
||||
capture: (
|
||||
workspaceRoot: string
|
||||
) => Promise<XtreamBenchmarkBuildIdentity> = captureXtreamBuildIdentity
|
||||
): Promise<void> {
|
||||
let current: XtreamBenchmarkBuildIdentity;
|
||||
try {
|
||||
current = parseXtreamBenchmarkBuildIdentity(
|
||||
await capture(configuration.workspaceRoot)
|
||||
);
|
||||
} catch {
|
||||
throw new Error('Xtream performance build changed during the suite');
|
||||
}
|
||||
if (!isDeepStrictEqual(configuration.build, current)) {
|
||||
throw new Error('Xtream performance build changed during the suite');
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSmokeMode(value: string | undefined): boolean {
|
||||
if (value === undefined) return false;
|
||||
if (value === '1') return true;
|
||||
throw new Error('IPTVNATOR_PERF_SMOKE is invalid');
|
||||
}
|
||||
|
||||
function assertSourceState(
|
||||
mode: 'formal' | 'smoke',
|
||||
source: XtreamGitSourceState
|
||||
): void {
|
||||
if (mode === 'formal' && source.gitDirty) {
|
||||
throw new Error(
|
||||
'Formal Xtream performance runs require a clean Git worktree'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function readGitSourceState(repositoryRoot: string): XtreamGitSourceState {
|
||||
const git = (arguments_: readonly string[]): string =>
|
||||
execFileSync('git', [...arguments_], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const gitCommit = git(['rev-parse', 'HEAD']).trim();
|
||||
const status = git(['status', '--porcelain=v1', '--untracked-files=all']);
|
||||
const gitDiffSha256 = createHash('sha256')
|
||||
.update(status)
|
||||
.update('\0')
|
||||
.update(git(['diff', '--binary', 'HEAD']))
|
||||
.update('\0')
|
||||
.update(git(['diff', '--binary', '--cached', 'HEAD']))
|
||||
.digest('hex');
|
||||
return Object.freeze({
|
||||
gitCommit,
|
||||
gitDiffSha256,
|
||||
gitDirty: status.trim().length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function readElectronRuntimeVersion(
|
||||
repositoryRoot: string
|
||||
): Promise<string> {
|
||||
const value = JSON.parse(
|
||||
await readFile(
|
||||
join(repositoryRoot, 'node_modules', 'electron', 'package.json'),
|
||||
'utf8'
|
||||
)
|
||||
) as { version?: unknown };
|
||||
if (typeof value.version !== 'string' || !value.version.trim()) {
|
||||
throw new Error('Unable to resolve the Electron runtime version');
|
||||
}
|
||||
return value.version;
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
assertXtreamArtifactRedacted,
|
||||
assertXtreamControlReadyAfterObservationReset,
|
||||
assertXtreamControlStateForScenario,
|
||||
assertXtreamManifest,
|
||||
assertXtreamManifestIdentity,
|
||||
assertXtreamMockOrigin,
|
||||
createXtreamIterationDefinitions,
|
||||
listXtreamBenchmarkScenarios,
|
||||
XTREAM_ITERATION_KIND,
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamControlState,
|
||||
type XtreamPerformanceManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
|
||||
const MANIFEST: XtreamPerformanceManifest = {
|
||||
epoch: 1,
|
||||
scenario: 'performance-100k',
|
||||
seed: 91_001,
|
||||
counts: {
|
||||
categories: { live: 60, vod: 20, series: 20, total: 100 },
|
||||
items: {
|
||||
live: 60_000,
|
||||
vod: 20_000,
|
||||
series: 20_000,
|
||||
total: 100_000,
|
||||
},
|
||||
},
|
||||
bytes: 12_345_678,
|
||||
catalogSha256:
|
||||
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
};
|
||||
|
||||
describe('Xtream benchmark contract', () => {
|
||||
it('defines exactly the five requested scenarios', () => {
|
||||
assert.deepEqual(
|
||||
listXtreamBenchmarkScenarios().map(({ id }) => id),
|
||||
[
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
XTREAM_SCENARIO_ID.REFRESH_LARGE,
|
||||
XTREAM_SCENARIO_ID.DELETE_LARGE,
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT,
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI,
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('schedules warm-up, five comparable measurements, and one diagnostic', () => {
|
||||
const definitions = createXtreamIterationDefinitions(false);
|
||||
|
||||
assert.equal(definitions.length, 35);
|
||||
for (const { id } of listXtreamBenchmarkScenarios()) {
|
||||
const runs = definitions.filter(
|
||||
(definition) => definition.scenarioId === id
|
||||
);
|
||||
assert.deepEqual(
|
||||
runs.map(({ eligibleForComparison, kind, runId }) => ({
|
||||
eligibleForComparison,
|
||||
kind,
|
||||
runId,
|
||||
})),
|
||||
[
|
||||
{
|
||||
eligibleForComparison: false,
|
||||
kind: XTREAM_ITERATION_KIND.WARMUP,
|
||||
runId: 'warmup-01',
|
||||
},
|
||||
...Array.from({ length: 5 }, (_, index) => ({
|
||||
eligibleForComparison: true,
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: `run-${String(index + 1).padStart(2, '0')}`,
|
||||
})),
|
||||
{
|
||||
eligibleForComparison: false,
|
||||
kind: XTREAM_ITERATION_KIND.DIAGNOSTIC,
|
||||
runId: 'diagnostic',
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps smoke measurements ineligible for formal comparison', () => {
|
||||
const definitions = createXtreamIterationDefinitions(true);
|
||||
|
||||
assert.equal(definitions.length, 15);
|
||||
assert.ok(
|
||||
definitions.every((definition) => !definition.eligibleForComparison)
|
||||
);
|
||||
for (const { id } of listXtreamBenchmarkScenarios()) {
|
||||
assert.deepEqual(
|
||||
definitions
|
||||
.filter((definition) => definition.scenarioId === id)
|
||||
.map(({ kind, runId }) => ({ kind, runId })),
|
||||
[
|
||||
{
|
||||
kind: XTREAM_ITERATION_KIND.WARMUP,
|
||||
runId: 'warmup-01',
|
||||
},
|
||||
{
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: 'run-01',
|
||||
},
|
||||
{
|
||||
kind: XTREAM_ITERATION_KIND.DIAGNOSTIC,
|
||||
runId: 'diagnostic',
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts only an exact credential-free HTTP loopback origin', () => {
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamMockOrigin('http://127.0.0.1:43123')
|
||||
);
|
||||
|
||||
for (const unsafe of [
|
||||
'https://127.0.0.1:43123',
|
||||
'http://localhost:43123',
|
||||
'http://127.0.0.1',
|
||||
'http://127.0.0.1:43123/',
|
||||
'http://user:pass@127.0.0.1:43123',
|
||||
'http://127.0.0.1:43123/player_api.php',
|
||||
'http://127.0.0.1:43123?token=secret',
|
||||
'http://127.0.0.1:43123#fragment',
|
||||
]) {
|
||||
assert.throws(
|
||||
() => assertXtreamMockOrigin(unsafe),
|
||||
/exact Xtream HTTP loopback origin/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects unexpected fixture names, counts, hashes, and identity drift', () => {
|
||||
assert.doesNotThrow(() => assertXtreamManifest(MANIFEST));
|
||||
|
||||
for (const invalid of [
|
||||
{ ...MANIFEST, scenario: 'provider-data' },
|
||||
{ ...MANIFEST, seed: 42 },
|
||||
{
|
||||
...MANIFEST,
|
||||
counts: {
|
||||
...MANIFEST.counts,
|
||||
items: { ...MANIFEST.counts.items, total: 99_999 },
|
||||
},
|
||||
},
|
||||
{ ...MANIFEST, catalogSha256: 'not-a-hash' },
|
||||
{ ...MANIFEST, username: 'performance' },
|
||||
]) {
|
||||
assert.throws(
|
||||
() => assertXtreamManifest(invalid),
|
||||
/invalid Xtream performance manifest/
|
||||
);
|
||||
}
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamManifestIdentity(MANIFEST, {
|
||||
...MANIFEST,
|
||||
epoch: 2,
|
||||
})
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamManifestIdentity(MANIFEST, {
|
||||
...MANIFEST,
|
||||
epoch: 2,
|
||||
bytes: MANIFEST.bytes + 1,
|
||||
}),
|
||||
/fixture identity changed/
|
||||
);
|
||||
});
|
||||
|
||||
it('requires a clean, same-epoch pre-trigger state after observation reset', () => {
|
||||
const ready = controlState([]);
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamControlReadyAfterObservationReset(ready, MANIFEST)
|
||||
);
|
||||
|
||||
for (const invalid of [
|
||||
{ ...ready, epoch: 2 },
|
||||
{ ...ready, rules: [{ kind: 'barrier' }] },
|
||||
{ ...ready, heldCount: 1, heldIds: ['leftover'] },
|
||||
controlState([['get_account_info', 'all', 1]]),
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamControlReadyAfterObservationReset(
|
||||
invalid,
|
||||
MANIFEST
|
||||
),
|
||||
/invalid Xtream mock readiness/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows category requests to arrive in any parallel order', () => {
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamControlStateForScenario(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
controlState(
|
||||
[
|
||||
['get_account_info', 'all', 1],
|
||||
['get_series_categories', 'all', 2],
|
||||
['get_vod_categories', 'all', 3],
|
||||
['get_live_categories', 'all', 4],
|
||||
['get_live_streams', 'all', 5],
|
||||
['get_vod_streams', 'all', 6],
|
||||
['get_series', 'all', 7],
|
||||
],
|
||||
2
|
||||
),
|
||||
MANIFEST
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('fails closed on action drift, proxy traffic, residue, and ordering drift', () => {
|
||||
const cases: XtreamControlState[] = [
|
||||
controlState(fullActions().slice(0, -1), 2),
|
||||
controlState([...fullActions(), ['get_vod_info', 'all', 8]], 2),
|
||||
{
|
||||
...controlState(fullActions(), 2),
|
||||
occurrences: controlState(fullActions(), 2).occurrences.map(
|
||||
(entry, index) =>
|
||||
index === 0 ? { ...entry, transport: 'proxy' } : entry
|
||||
),
|
||||
},
|
||||
{
|
||||
...controlState(fullActions(), 2),
|
||||
rules: [{ kind: 'delay' }],
|
||||
},
|
||||
{
|
||||
...controlState(fullActions(), 2),
|
||||
heldCount: 1,
|
||||
heldIds: ['leftover'],
|
||||
},
|
||||
controlState(
|
||||
[
|
||||
['get_account_info', 'all', 1],
|
||||
['get_live_streams', 'all', 2],
|
||||
['get_live_categories', 'all', 3],
|
||||
['get_vod_categories', 'all', 4],
|
||||
['get_series_categories', 'all', 5],
|
||||
['get_vod_streams', 'all', 6],
|
||||
['get_series', 'all', 7],
|
||||
],
|
||||
2
|
||||
),
|
||||
];
|
||||
|
||||
for (const invalid of cases) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamControlStateForScenario(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
invalid,
|
||||
MANIFEST
|
||||
),
|
||||
/invalid Xtream mock observations/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects credential keys and secret values from persisted artifacts', () => {
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamArtifactRedacted({
|
||||
fixture: {
|
||||
bytes: MANIFEST.bytes,
|
||||
catalogSha256: MANIFEST.catalogSha256,
|
||||
},
|
||||
})
|
||||
);
|
||||
for (const unsafe of [
|
||||
{ username: 'performance' },
|
||||
{ nested: { password: 'performance' } },
|
||||
{ serverUrl: 'http://127.0.0.1:43123' },
|
||||
{ note: 'leaked benchmark-control-token' },
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamArtifactRedacted(unsafe, [
|
||||
'benchmark-control-token',
|
||||
]),
|
||||
/credential-safe/
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
type Action = readonly [string, string, number];
|
||||
|
||||
function fullActions(): readonly Action[] {
|
||||
return [
|
||||
['get_account_info', 'all', 1],
|
||||
['get_live_categories', 'all', 2],
|
||||
['get_vod_categories', 'all', 3],
|
||||
['get_series_categories', 'all', 4],
|
||||
['get_live_streams', 'all', 5],
|
||||
['get_vod_streams', 'all', 6],
|
||||
['get_series', 'all', 7],
|
||||
];
|
||||
}
|
||||
|
||||
function controlState(
|
||||
actions: readonly Action[],
|
||||
accountCount = 1
|
||||
): XtreamControlState {
|
||||
return {
|
||||
epoch: MANIFEST.epoch,
|
||||
prepared: MANIFEST,
|
||||
rules: [],
|
||||
heldIds: [],
|
||||
heldCount: 0,
|
||||
occurrences: actions.map(([action, categoryId]) => ({
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct',
|
||||
action,
|
||||
categoryId,
|
||||
count: action === 'get_account_info' ? accountCount : 1,
|
||||
})),
|
||||
ledger: actions.flatMap(([action, categoryId, order]) =>
|
||||
Array.from(
|
||||
{
|
||||
length: action === 'get_account_info' ? accountCount : 1,
|
||||
},
|
||||
(_, index) => {
|
||||
const shiftedOrder =
|
||||
order +
|
||||
index +
|
||||
(action === 'get_account_info' ? 0 : accountCount - 1);
|
||||
return [
|
||||
{
|
||||
epoch: MANIFEST.epoch,
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct' as const,
|
||||
action,
|
||||
categoryId,
|
||||
occurrence: index + 1,
|
||||
status: 'arrived' as const,
|
||||
atMs: shiftedOrder * 10,
|
||||
},
|
||||
{
|
||||
epoch: MANIFEST.epoch,
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct' as const,
|
||||
action,
|
||||
categoryId,
|
||||
occurrence: index + 1,
|
||||
status: 'responded' as const,
|
||||
atMs: shiftedOrder * 10 + 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
).flat()
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import type { XTREAM_MEMORY_ACCOUNTING } from './xtream-summary-contract';
|
||||
import type { XtreamBenchmarkBuildIdentity } from './xtream-build-identity';
|
||||
|
||||
export const XTREAM_SCENARIO_ID = {
|
||||
INITIAL_IMPORT_LARGE: 'xtream-initial-import-large',
|
||||
REFRESH_LARGE: 'xtream-refresh-large',
|
||||
DELETE_LARGE: 'xtream-delete-large',
|
||||
CANCEL_IMPORT: 'xtream-cancel-import',
|
||||
BACKGROUND_UI: 'xtream-background-ui',
|
||||
} as const;
|
||||
|
||||
export type XtreamScenarioId =
|
||||
(typeof XTREAM_SCENARIO_ID)[keyof typeof XTREAM_SCENARIO_ID];
|
||||
|
||||
export const XTREAM_ITERATION_KIND = {
|
||||
DIAGNOSTIC: 'diagnostic',
|
||||
MEASURED: 'measured',
|
||||
WARMUP: 'warmup',
|
||||
} as const;
|
||||
|
||||
export type XtreamIterationKind =
|
||||
(typeof XTREAM_ITERATION_KIND)[keyof typeof XTREAM_ITERATION_KIND];
|
||||
|
||||
export interface XtreamScenario {
|
||||
readonly id: XtreamScenarioId;
|
||||
}
|
||||
|
||||
export interface XtreamWorkerRequestInventoryEntry {
|
||||
readonly count: number;
|
||||
readonly operation: string;
|
||||
readonly phases: readonly string[];
|
||||
readonly success: boolean;
|
||||
}
|
||||
|
||||
export interface XtreamIterationDefinition {
|
||||
readonly eligibleForComparison: boolean;
|
||||
readonly kind: XtreamIterationKind;
|
||||
readonly runId: string;
|
||||
readonly scenarioId: XtreamScenarioId;
|
||||
}
|
||||
|
||||
export interface XtreamPerformanceManifest {
|
||||
readonly bytes: number;
|
||||
readonly catalogSha256: string;
|
||||
readonly counts: {
|
||||
readonly categories: XtreamCatalogCounts;
|
||||
readonly items: XtreamCatalogCounts;
|
||||
};
|
||||
readonly epoch: number;
|
||||
readonly scenario: 'performance-100k';
|
||||
readonly seed: 91_001;
|
||||
}
|
||||
|
||||
export interface XtreamCatalogCounts {
|
||||
readonly live: number;
|
||||
readonly series: number;
|
||||
readonly total: number;
|
||||
readonly vod: number;
|
||||
}
|
||||
|
||||
export interface XtreamOccurrence {
|
||||
readonly action: string;
|
||||
readonly categoryId: string;
|
||||
readonly count: number;
|
||||
readonly scenario: string;
|
||||
readonly transport: string;
|
||||
}
|
||||
|
||||
export interface XtreamLedgerEntry {
|
||||
readonly action: string;
|
||||
readonly atMs: number;
|
||||
readonly categoryId: string;
|
||||
readonly epoch: number;
|
||||
readonly occurrence: number;
|
||||
readonly scenario: string;
|
||||
readonly status: string;
|
||||
readonly transport: string;
|
||||
}
|
||||
|
||||
export interface XtreamControlState {
|
||||
readonly epoch: number;
|
||||
readonly heldCount: number;
|
||||
readonly heldIds: readonly string[];
|
||||
readonly ledger: readonly XtreamLedgerEntry[];
|
||||
readonly occurrences: readonly XtreamOccurrence[];
|
||||
readonly prepared: XtreamPerformanceManifest | null;
|
||||
readonly rules: readonly unknown[];
|
||||
}
|
||||
|
||||
export interface XtreamBenchmarkManifest {
|
||||
readonly build: XtreamBenchmarkBuildIdentity;
|
||||
readonly environment: XtreamBenchmarkEnvironment;
|
||||
readonly fixture: XtreamBenchmarkFixture;
|
||||
readonly memoryAccounting: typeof XTREAM_MEMORY_ACCOUNTING;
|
||||
readonly mode: 'formal' | 'smoke';
|
||||
readonly runtime: XtreamBenchmarkRuntime;
|
||||
readonly source: XtreamBenchmarkSource;
|
||||
readonly suite: 'xtream-performance-100k';
|
||||
readonly variant: string;
|
||||
}
|
||||
|
||||
export interface XtreamBenchmarkEnvironment {
|
||||
readonly cdpAddress: '127.0.0.1';
|
||||
readonly cdpPort: number;
|
||||
readonly databaseWorkerBatchDelayMs: 0;
|
||||
readonly exposeGc: true;
|
||||
readonly freshUserDataPerIteration: true;
|
||||
readonly perfCapture: '1';
|
||||
readonly perfWorkerProfiling: '1';
|
||||
readonly traceRendererConsole: '0';
|
||||
}
|
||||
|
||||
export interface XtreamBenchmarkFixture {
|
||||
readonly bytes: number;
|
||||
readonly catalogSha256: string;
|
||||
readonly categories: 100;
|
||||
readonly items: 100_000;
|
||||
readonly scenario: 'performance-100k';
|
||||
readonly seed: 91_001;
|
||||
}
|
||||
|
||||
export interface XtreamBenchmarkRuntime {
|
||||
readonly arch: string;
|
||||
readonly electronVersion: string;
|
||||
readonly harnessNodeVersion: string;
|
||||
readonly platform: string;
|
||||
}
|
||||
|
||||
export interface XtreamBenchmarkSource {
|
||||
readonly gitCommit: string;
|
||||
readonly gitDiffSha256: string;
|
||||
readonly gitDirty: boolean;
|
||||
}
|
||||
|
||||
export interface NumericDistribution {
|
||||
readonly count: number;
|
||||
readonly max: number | null;
|
||||
readonly mean: number | null;
|
||||
readonly median: number | null;
|
||||
readonly min: number | null;
|
||||
readonly p95: number | null;
|
||||
readonly p99: number | null;
|
||||
}
|
||||
|
||||
export function summarizeXtreamNumbers(
|
||||
values: readonly number[]
|
||||
): NumericDistribution {
|
||||
const samples = [...values].sort((left, right) => left - right);
|
||||
const total = samples.reduce((sum, value) => sum + value, 0);
|
||||
const percentile = (rank: number): number => {
|
||||
if (samples.length === 1) return samples[0] ?? 0;
|
||||
const position = ((samples.length - 1) * rank) / 100;
|
||||
const lowerIndex = Math.floor(position);
|
||||
const upperIndex = Math.ceil(position);
|
||||
const lower = samples[lowerIndex] ?? 0;
|
||||
const upper = samples[upperIndex] ?? lower;
|
||||
return lower + (upper - lower) * (position - lowerIndex);
|
||||
};
|
||||
return {
|
||||
count: samples.length,
|
||||
max: samples.at(-1) ?? null,
|
||||
mean: total / samples.length,
|
||||
median: percentile(50),
|
||||
min: samples[0] ?? null,
|
||||
p95: percentile(95),
|
||||
p99: percentile(99),
|
||||
};
|
||||
}
|
||||
|
||||
export interface XtreamScenarioMetrics {
|
||||
readonly angularRenderingMs: NumericDistribution;
|
||||
readonly cancelAcknowledgementLatencyMs: NumericDistribution | null;
|
||||
readonly dataAcquisitionMs: NumericDistribution;
|
||||
readonly deserializationMs: NumericDistribution;
|
||||
readonly databaseWorkerCpuSystemMicros: NumericDistribution;
|
||||
readonly databaseWorkerCpuUserMicros: NumericDistribution;
|
||||
readonly databaseWorkerEventLoopDelayMaxMs: NumericDistribution;
|
||||
readonly databaseWorkerEventLoopDelayP95Ms: NumericDistribution;
|
||||
readonly databaseWorkerEventLoopDelayP99Ms: NumericDistribution;
|
||||
readonly databaseWorkerEventLoopUtilization: NumericDistribution;
|
||||
readonly databaseWorkerPeakExternalBytes: NumericDistribution;
|
||||
readonly databaseWorkerPeakHeapBytes: NumericDistribution;
|
||||
readonly databaseWorkerPostGcHeapBytes: NumericDistribution;
|
||||
readonly databaseWorkerRequestInvalidSampleCount: NumericDistribution;
|
||||
readonly databaseWorkerRequestEventLoopDelayMaxMs: NumericDistribution;
|
||||
readonly databaseWorkerRequestEventLoopDelayP95Ms: NumericDistribution;
|
||||
readonly databaseWorkerRequestEventLoopDelayP99Ms: NumericDistribution;
|
||||
readonly databaseWorkerRequestEventLoopUtilization: NumericDistribution;
|
||||
readonly databaseWorkerRequestTotalCount: NumericDistribution;
|
||||
readonly databaseWorkerRequestValidSampleCount: NumericDistribution;
|
||||
readonly jsonParsingMs: NumericDistribution;
|
||||
readonly mainCpuSystemMicros: NumericDistribution;
|
||||
readonly mainCpuUserMicros: NumericDistribution;
|
||||
readonly mainEventLoopDelayMaxMs: NumericDistribution;
|
||||
readonly mainEventLoopDelayP95Ms: NumericDistribution;
|
||||
readonly mainEventLoopDelayP99Ms: NumericDistribution;
|
||||
readonly mainPeakHeapBytes: NumericDistribution;
|
||||
readonly mainPeakRssBytes: NumericDistribution;
|
||||
readonly mainPostGcHeapBytes: NumericDistribution;
|
||||
readonly normalizationMs: NumericDistribution;
|
||||
readonly rendererActionLatencyMs: NumericDistribution | null;
|
||||
readonly rendererFrameGapMaxMs: NumericDistribution;
|
||||
readonly rendererFrameGapP95Ms: NumericDistribution;
|
||||
readonly rendererHeartbeatMaxMs: NumericDistribution;
|
||||
readonly rendererHeartbeatP95Ms: NumericDistribution;
|
||||
readonly rendererLongTaskCount: NumericDistribution;
|
||||
readonly rendererLongTaskMaxMs: NumericDistribution;
|
||||
readonly rendererNavigationLatencyMs: NumericDistribution | null;
|
||||
readonly rendererPeakHeapBytes: NumericDistribution;
|
||||
readonly rendererPeakRssBytes: NumericDistribution;
|
||||
readonly rendererPostGcHeapBytes: NumericDistribution;
|
||||
readonly rendererSearchLatencyMs: NumericDistribution | null;
|
||||
readonly serializationMs: NumericDistribution;
|
||||
readonly sqliteReadMs: NumericDistribution;
|
||||
readonly sqliteWriteMs: NumericDistribution;
|
||||
readonly storeUpdateMs: NumericDistribution;
|
||||
readonly structuredCloneProxyMs: NumericDistribution;
|
||||
readonly totalMs: NumericDistribution;
|
||||
}
|
||||
|
||||
const SCENARIOS: readonly XtreamScenario[] = Object.freeze([
|
||||
Object.freeze({ id: XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE }),
|
||||
Object.freeze({ id: XTREAM_SCENARIO_ID.REFRESH_LARGE }),
|
||||
Object.freeze({ id: XTREAM_SCENARIO_ID.DELETE_LARGE }),
|
||||
Object.freeze({ id: XTREAM_SCENARIO_ID.CANCEL_IMPORT }),
|
||||
Object.freeze({ id: XTREAM_SCENARIO_ID.BACKGROUND_UI }),
|
||||
]);
|
||||
|
||||
export function listXtreamBenchmarkScenarios(): readonly XtreamScenario[] {
|
||||
return SCENARIOS;
|
||||
}
|
||||
|
||||
export function createXtreamIterationDefinitions(
|
||||
smoke: boolean
|
||||
): readonly XtreamIterationDefinition[] {
|
||||
const definitions: XtreamIterationDefinition[] = [];
|
||||
for (const scenario of SCENARIOS) {
|
||||
definitions.push(iteration(scenario.id, 'warmup-01', 'warmup', false));
|
||||
for (let index = 1; index <= (smoke ? 1 : 5); index += 1) {
|
||||
definitions.push(
|
||||
iteration(
|
||||
scenario.id,
|
||||
`run-${String(index).padStart(2, '0')}`,
|
||||
'measured',
|
||||
!smoke
|
||||
)
|
||||
);
|
||||
}
|
||||
definitions.push(
|
||||
iteration(scenario.id, 'diagnostic', 'diagnostic', false)
|
||||
);
|
||||
}
|
||||
return Object.freeze(definitions);
|
||||
}
|
||||
|
||||
export function expectedXtreamWorkerRequestInventory(
|
||||
scenarioId: XtreamScenarioId
|
||||
): readonly XtreamWorkerRequestInventoryEntry[] {
|
||||
return WORKER_REQUEST_INVENTORY[scenarioId];
|
||||
}
|
||||
|
||||
const PHASES = Object.freeze({
|
||||
categoriesRead: ['sqlite.categories.read'],
|
||||
categoriesWrite: [
|
||||
'normalize.categories',
|
||||
'sqlite.categories.write-transactions',
|
||||
],
|
||||
contentRead: ['sqlite.content.read'],
|
||||
contentWrite: [
|
||||
'sqlite.content.category-map-read',
|
||||
'normalize.content',
|
||||
'sqlite.content.write-transactions',
|
||||
],
|
||||
playlistDelete: [
|
||||
'sqlite.playlist-delete.collect-ids',
|
||||
'sqlite.playlist-delete.write-transactions',
|
||||
],
|
||||
playlistGet: ['sqlite.read', 'deserialize.playlist'],
|
||||
playlistUpsert: ['serialize.playlist', 'sqlite.write'],
|
||||
xtreamCacheClear: ['sqlite.xtream-cache-clear.write-transactions'],
|
||||
xtreamDelete: [
|
||||
'sqlite.xtream-delete.collect-user-data',
|
||||
'sqlite.xtream-delete.write-transactions',
|
||||
],
|
||||
});
|
||||
|
||||
const WORKER_REQUEST_INVENTORY: Readonly<
|
||||
Record<XtreamScenarioId, readonly XtreamWorkerRequestInventoryEntry[]>
|
||||
> = Object.freeze({
|
||||
[XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE]: inventory(
|
||||
request('DB_UPSERT_APP_PLAYLIST', 1, PHASES.playlistUpsert),
|
||||
request('DB_GET_PLAYLIST', 1),
|
||||
request('DB_GET_ALL_PLAYBACK_POSITIONS', 1),
|
||||
...fullImportRequests()
|
||||
),
|
||||
[XTREAM_SCENARIO_ID.REFRESH_LARGE]: refreshRequests(),
|
||||
[XTREAM_SCENARIO_ID.DELETE_LARGE]: inventory(
|
||||
request('DB_DELETE_PLAYLIST', 2, PHASES.playlistDelete)
|
||||
),
|
||||
[XTREAM_SCENARIO_ID.CANCEL_IMPORT]: inventory(
|
||||
request('DB_UPSERT_APP_PLAYLIST', 1, PHASES.playlistUpsert),
|
||||
request('DB_GET_PLAYLIST', 1),
|
||||
request('DB_GET_ALL_PLAYBACK_POSITIONS', 1),
|
||||
request('DB_GET_APP_STATE', 4),
|
||||
request('DB_GET_CATEGORIES', 6, PHASES.categoriesRead),
|
||||
request('DB_SAVE_CATEGORIES', 3, PHASES.categoriesWrite),
|
||||
request('DB_GET_CONTENT', 1, PHASES.contentRead),
|
||||
request('DB_SAVE_CONTENT', 1, PHASES.contentWrite, false),
|
||||
request('DB_SET_APP_STATE', 3),
|
||||
request('DB_CLEAR_XTREAM_IMPORT_CACHE', 3, PHASES.xtreamCacheClear)
|
||||
),
|
||||
[XTREAM_SCENARIO_ID.BACKGROUND_UI]: inventory(
|
||||
...refreshRequests(),
|
||||
request('DB_GET_RECENTLY_VIEWED', 1),
|
||||
request('DB_GET_ALL_GLOBAL_FAVORITES', 1),
|
||||
request('DB_GET_GLOBAL_RECENTLY_ADDED', 1)
|
||||
),
|
||||
});
|
||||
|
||||
function fullImportRequests(
|
||||
reads: Readonly<{
|
||||
appState: number;
|
||||
categories: number;
|
||||
content: number;
|
||||
setAppState: number;
|
||||
}> = { appState: 6, categories: 6, content: 6, setAppState: 3 }
|
||||
): readonly XtreamWorkerRequestInventoryEntry[] {
|
||||
return inventory(
|
||||
request('DB_GET_APP_STATE', reads.appState),
|
||||
request('DB_GET_CATEGORIES', reads.categories, PHASES.categoriesRead),
|
||||
request('DB_SAVE_CATEGORIES', 3, PHASES.categoriesWrite),
|
||||
request('DB_GET_CONTENT', reads.content, PHASES.contentRead),
|
||||
request('DB_SAVE_CONTENT', 3, PHASES.contentWrite),
|
||||
request('DB_SET_APP_STATE', reads.setAppState)
|
||||
);
|
||||
}
|
||||
|
||||
function refreshRequests(): readonly XtreamWorkerRequestInventoryEntry[] {
|
||||
return inventory(
|
||||
request('DB_DELETE_XTREAM_CONTENT', 1, PHASES.xtreamDelete),
|
||||
request('DB_GET_ALL_PLAYBACK_POSITIONS', 1),
|
||||
request('DB_UPDATE_PLAYLIST', 1),
|
||||
request('DB_GET_APP_PLAYLIST', 1, PHASES.playlistGet),
|
||||
request('DB_GET_PLAYLIST', 2),
|
||||
request('DB_UPSERT_APP_PLAYLIST', 1, PHASES.playlistUpsert),
|
||||
...fullImportRequests({
|
||||
appState: 12,
|
||||
categories: 9,
|
||||
content: 9,
|
||||
setAppState: 6,
|
||||
}),
|
||||
request('DB_RESTORE_XTREAM_USER_DATA', 1),
|
||||
request('DB_CLEAR_ALL_PLAYBACK_POSITIONS', 1)
|
||||
);
|
||||
}
|
||||
|
||||
function inventory(
|
||||
...entries: readonly XtreamWorkerRequestInventoryEntry[]
|
||||
): readonly XtreamWorkerRequestInventoryEntry[] {
|
||||
return Object.freeze(entries);
|
||||
}
|
||||
|
||||
function request(
|
||||
operation: string,
|
||||
count: number,
|
||||
phases: readonly string[] = [],
|
||||
success = true
|
||||
): XtreamWorkerRequestInventoryEntry {
|
||||
return Object.freeze({
|
||||
count,
|
||||
operation,
|
||||
phases: Object.freeze([...phases]),
|
||||
success,
|
||||
});
|
||||
}
|
||||
|
||||
function iteration(
|
||||
scenarioId: XtreamScenarioId,
|
||||
runId: string,
|
||||
kind: XtreamIterationKind,
|
||||
eligibleForComparison: boolean
|
||||
): XtreamIterationDefinition {
|
||||
return Object.freeze({
|
||||
eligibleForComparison,
|
||||
kind,
|
||||
runId,
|
||||
scenarioId,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
assertXtreamArtifactRedacted,
|
||||
assertXtreamControlReadyAfterObservationReset,
|
||||
assertXtreamControlStateForScenario,
|
||||
assertXtreamManifest,
|
||||
assertXtreamManifestIdentity,
|
||||
assertXtreamMockOrigin,
|
||||
parseXtreamControlState,
|
||||
} from './xtream-control-client';
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
|
||||
import type { XtreamBenchmarkManifest } from './xtream-benchmark-contract';
|
||||
import { createXtreamBenchmarkSummary } from './xtream-benchmark-report';
|
||||
import {
|
||||
MANIFEST,
|
||||
clearXtreamReportFixtureDirectories,
|
||||
} from './xtream-benchmark-report.fixtures';
|
||||
import {
|
||||
clearXtreamProvenanceFixtureDirectories,
|
||||
smokeReportFixture,
|
||||
} from './xtream-benchmark-report-provenance.fixtures';
|
||||
import { assertXtreamBenchmarkManifest } from './xtream-exact-schema';
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
clearXtreamReportFixtureDirectories(),
|
||||
clearXtreamProvenanceFixtureDirectories(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Xtream benchmark CDP environment', () => {
|
||||
it('allows a bounded alternate smoke port but keeps formal capture on 9222', async () => {
|
||||
const smokeManifest: XtreamBenchmarkManifest = {
|
||||
...MANIFEST,
|
||||
environment: { ...MANIFEST.environment, cdpPort: 9322 },
|
||||
mode: 'smoke',
|
||||
};
|
||||
assert.doesNotThrow(() => assertXtreamBenchmarkManifest(smokeManifest));
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamBenchmarkManifest({
|
||||
...smokeManifest,
|
||||
mode: 'formal',
|
||||
}),
|
||||
/invalid Xtream benchmark manifest/
|
||||
);
|
||||
|
||||
const { diagnosticEvidence, iterations } = await smokeReportFixture();
|
||||
const summary = createXtreamBenchmarkSummary(
|
||||
smokeManifest,
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
);
|
||||
assert.equal(summary.validForComparison, false);
|
||||
});
|
||||
|
||||
it('requires an integer CDP port from 1024 through 65535', () => {
|
||||
for (const cdpPort of [1023, 65536, 9322.5, Number.NaN]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamBenchmarkManifest({
|
||||
...MANIFEST,
|
||||
environment: { ...MANIFEST.environment, cdpPort },
|
||||
mode: 'smoke',
|
||||
}),
|
||||
/invalid Xtream benchmark manifest/
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import { createXtreamIterationDefinitions } from './xtream-benchmark-contract';
|
||||
import { createXtreamBenchmarkSummary } from './xtream-benchmark-report';
|
||||
import {
|
||||
MANIFEST,
|
||||
clearXtreamReportFixtureDirectories,
|
||||
iteration,
|
||||
persist,
|
||||
persistWith,
|
||||
sha256,
|
||||
} from './xtream-benchmark-report.fixtures';
|
||||
import { toXtreamRawIteration } from './xtream-exact-schema';
|
||||
import {
|
||||
clearXtreamProvenanceFixtureDirectories,
|
||||
formalReportFixture,
|
||||
} from './xtream-benchmark-report-provenance.fixtures';
|
||||
import type {
|
||||
XtreamBenchmarkManifest,
|
||||
XtreamPersistenceReceipt,
|
||||
} from './xtream-summary-contract';
|
||||
|
||||
describe('Xtream benchmark integrity boundaries', () => {
|
||||
it('uses one intrinsic snapshot instead of caller-owned array methods', async () => {
|
||||
const { diagnosticEvidence, iterations } = await formalReportFixture();
|
||||
const adversarial = [...iterations];
|
||||
const invoked: string[] = [];
|
||||
for (const method of ['filter', 'find', 'map'] as const) {
|
||||
Object.defineProperty(adversarial, method, {
|
||||
value() {
|
||||
invoked.push(method);
|
||||
throw new Error(`caller-owned-${method}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
Object.defineProperty(adversarial, Symbol.iterator, {
|
||||
value() {
|
||||
invoked.push('iterator');
|
||||
throw new Error('caller-owned-iterator');
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
assert.doesNotThrow(() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
adversarial,
|
||||
diagnosticEvidence
|
||||
)
|
||||
);
|
||||
assert.deepEqual(invoked, []);
|
||||
} finally {
|
||||
await clearXtreamProvenanceFixtureDirectories();
|
||||
}
|
||||
});
|
||||
|
||||
it('cannot hide a reused process identity behind caller-owned map', async () => {
|
||||
const fixture = await formalReportFixture();
|
||||
const definitions = createXtreamIterationDefinitions(false);
|
||||
const iterations = [...fixture.iterations];
|
||||
const sourceDuplicate = iteration(required(definitions, 1), 9_999);
|
||||
const duplicate = {
|
||||
...sourceDuplicate,
|
||||
processIdentity: {
|
||||
...sourceDuplicate.processIdentity,
|
||||
electronPid: required(iterations, 0).processIdentity
|
||||
.electronPid,
|
||||
},
|
||||
};
|
||||
iterations[1] = await persist(duplicate, 9_999);
|
||||
Object.defineProperty(iterations, 'map', {
|
||||
value() {
|
||||
return Array.from(
|
||||
{ length: iterations.length },
|
||||
(_, index) => index
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
iterations,
|
||||
fixture.diagnosticEvidence
|
||||
),
|
||||
/fresh process identity/
|
||||
);
|
||||
} finally {
|
||||
await Promise.all([
|
||||
clearXtreamReportFixtureDirectories(),
|
||||
clearXtreamProvenanceFixtureDirectories(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses native Map entries after validating diagnostic evidence', async () => {
|
||||
const { diagnosticEvidence, iterations } = await formalReportFixture();
|
||||
const entries = [...diagnosticEvidence.entries()];
|
||||
const [firstIteration, firstEvidence] = required(entries, 0);
|
||||
const [, secondEvidence] = required(entries, 1);
|
||||
const adversarial = new Map(entries);
|
||||
Object.defineProperty(adversarial, 'get', {
|
||||
value(key: (typeof entries)[number][0]) {
|
||||
return key === firstIteration
|
||||
? secondEvidence
|
||||
: Map.prototype.get.call(this, key);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const summary = createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
iterations,
|
||||
adversarial
|
||||
);
|
||||
const scenario = summary.scenarios.find(
|
||||
({ scenarioId }) => scenarioId === firstIteration.scenarioId
|
||||
);
|
||||
assert.equal(
|
||||
scenario?.diagnosticProfiles.artifactProvenance.directorySha256,
|
||||
firstEvidence.directorySha256
|
||||
);
|
||||
} finally {
|
||||
await clearXtreamProvenanceFixtureDirectories();
|
||||
}
|
||||
});
|
||||
|
||||
it('snapshots every persistence receipt accessor exactly once', async () => {
|
||||
const definition = required(createXtreamIterationDefinitions(false), 1);
|
||||
const raw = iteration(definition, 1);
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-xtream-receipt-accessor-')
|
||||
);
|
||||
const regularPath = join(directory, 'raw.json');
|
||||
const symlinkPath = join(directory, 'raw-link.json');
|
||||
try {
|
||||
await assert.rejects(
|
||||
() =>
|
||||
persistWith(raw, async (serialized) => {
|
||||
await writeFile(regularPath, serialized);
|
||||
await symlink(regularPath, symlinkPath);
|
||||
let pathReads = 0;
|
||||
return {
|
||||
get absolutePath() {
|
||||
pathReads += 1;
|
||||
return pathReads === 1
|
||||
? symlinkPath
|
||||
: regularPath;
|
||||
},
|
||||
bytes: Buffer.byteLength(serialized),
|
||||
sha256: sha256(serialized),
|
||||
} as XtreamPersistenceReceipt;
|
||||
}),
|
||||
/persistence receipt/
|
||||
);
|
||||
|
||||
let stablePathReads = 0;
|
||||
await persistWith(raw, async (serialized) => ({
|
||||
get absolutePath() {
|
||||
stablePathReads += 1;
|
||||
return regularPath;
|
||||
},
|
||||
bytes: Buffer.byteLength(serialized),
|
||||
sha256: sha256(serialized),
|
||||
}));
|
||||
assert.equal(stablePathReads, 1);
|
||||
} finally {
|
||||
await rm(directory, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('uses one canonical manifest snapshot for validation and output', async () => {
|
||||
const { diagnosticEvidence, iterations } = await formalReportFixture();
|
||||
let variantReads = 0;
|
||||
const manifest = {
|
||||
...MANIFEST,
|
||||
get variant() {
|
||||
variantReads += 1;
|
||||
return variantReads <= 3 ? 'baseline' : 'CON.json';
|
||||
},
|
||||
} as XtreamBenchmarkManifest;
|
||||
|
||||
try {
|
||||
const summary = createXtreamBenchmarkSummary(
|
||||
manifest,
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
);
|
||||
assert.equal(summary.manifest.variant, 'baseline');
|
||||
assert.equal(variantReads, 1);
|
||||
} finally {
|
||||
await clearXtreamProvenanceFixtureDirectories();
|
||||
}
|
||||
});
|
||||
|
||||
it('binds diagnostic profile durations and worker ordinal to artifact evidence', async () => {
|
||||
const fixture = await formalReportFixture();
|
||||
const [diagnostic, evidence] = required(
|
||||
[...fixture.diagnosticEvidence.entries()],
|
||||
0
|
||||
);
|
||||
const diagnosticIndex = fixture.iterations.indexOf(diagnostic);
|
||||
assert.notEqual(diagnosticIndex, -1);
|
||||
const raw = toXtreamRawIteration(diagnostic);
|
||||
const mismatches = [
|
||||
{
|
||||
...raw,
|
||||
renderer: {
|
||||
...raw.renderer,
|
||||
cpuProfileDurationMicros: 2_001,
|
||||
},
|
||||
},
|
||||
{
|
||||
...raw,
|
||||
main: {
|
||||
...raw.main,
|
||||
cpuProfileDurationMicros: 2_001,
|
||||
},
|
||||
},
|
||||
{
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
cpuProfileDurationMicros: 2_001,
|
||||
},
|
||||
},
|
||||
{
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
ordinal: raw.databaseWorker.ordinal + 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
for (const [index, mismatch] of mismatches.entries()) {
|
||||
const replacement = await persist(mismatch, 20_000 + index);
|
||||
const iterations = [...fixture.iterations];
|
||||
iterations[diagnosticIndex] = replacement;
|
||||
const diagnosticEvidence = new Map(fixture.diagnosticEvidence);
|
||||
diagnosticEvidence.delete(diagnostic);
|
||||
diagnosticEvidence.set(replacement, evidence);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
),
|
||||
/diagnostic artifact evidence binding/
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await Promise.all([
|
||||
clearXtreamReportFixtureDirectories(),
|
||||
clearXtreamProvenanceFixtureDirectories(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function required<T>(values: readonly T[], index: number): T {
|
||||
const value = values[index];
|
||||
assert.ok(value);
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
ELECTRON_RUNTIME_ENVIRONMENT_KEYS,
|
||||
type LaunchedElectronApp,
|
||||
} from '../electron-test-fixtures';
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamIterationDefinition,
|
||||
type XtreamPerformanceManifest,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { writeXtreamSafeJson } from './xtream-benchmark-artifacts';
|
||||
import type { XtreamControlClient } from './xtream-control-client';
|
||||
import { assertXtreamControlStateForScenario } from './xtream-control-client';
|
||||
import {
|
||||
validateXtreamDiagnosticArtifacts,
|
||||
type XtreamDiagnosticArtifactEvidence,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import {
|
||||
readXtreamMainCaptureStatus,
|
||||
startMainCapture,
|
||||
type MainCaptureStartOptions,
|
||||
type XtreamMainCaptureResult,
|
||||
type XtreamMainCaptureStatus,
|
||||
} from './m3u-refresh-main-capture';
|
||||
import { isXtreamMainCaptureSettled } from './xtream-main-settlement';
|
||||
import type { XtreamRendererCaptureMetrics } from './xtream-renderer-capture';
|
||||
import {
|
||||
assertCompleteXtreamRendererProbe,
|
||||
beginXtreamRendererOperation,
|
||||
stopXtreamRendererProbe,
|
||||
waitForXtreamRendererTerminal,
|
||||
} from './xtream-renderer-probe-control';
|
||||
import { installXtreamRendererProbe } from './xtream-renderer-probe';
|
||||
import { prepareXtreamScenario } from './xtream-scenario-driver';
|
||||
import type { XtreamArtifactSafetyContext } from './xtream-summary-contract';
|
||||
|
||||
export interface SeedXtreamExistingPortalOptions {
|
||||
readonly app: LaunchedElectronApp;
|
||||
readonly captureOptions: MainCaptureStartOptions;
|
||||
readonly control: XtreamControlClient;
|
||||
readonly fixture: XtreamPerformanceManifest;
|
||||
readonly iterationDirectory: string;
|
||||
readonly safety: XtreamArtifactSafetyContext;
|
||||
readonly serverUrl: string;
|
||||
readonly sourceTitle: string;
|
||||
}
|
||||
|
||||
export async function prearmXtreamDatabaseWorker(
|
||||
app: LaunchedElectronApp
|
||||
): Promise<void> {
|
||||
const playlists = await app.mainWindow.evaluate(() =>
|
||||
window.electron.dbGetAppPlaylists()
|
||||
);
|
||||
if (!Array.isArray(playlists)) {
|
||||
throw new Error('xtream-database-worker-prearm-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
export async function seedXtreamExistingPortal(
|
||||
options: SeedXtreamExistingPortalOptions
|
||||
): Promise<void> {
|
||||
const seed = await prepareXtreamScenario(options.app.mainWindow, {
|
||||
scenarioId: XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
serverUrl: options.serverUrl,
|
||||
sourceTitle: options.sourceTitle,
|
||||
});
|
||||
let rendererProbeInstalled = false;
|
||||
try {
|
||||
await installXtreamRendererProbe(options.app.mainWindow, {
|
||||
scenarioId: XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
sourceTitle: options.sourceTitle,
|
||||
});
|
||||
rendererProbeInstalled = true;
|
||||
await startMainCapture(options.app.electronApp, {
|
||||
...options.captureOptions,
|
||||
deferMeasurement: false,
|
||||
diagnostic: false,
|
||||
});
|
||||
const captureGeneration = (
|
||||
await readXtreamMainCaptureStatus(options.app.electronApp)
|
||||
).captureGeneration;
|
||||
await beginXtreamRendererOperation(options.app.mainWindow);
|
||||
await seed.trigger();
|
||||
const driverTerminal = seed.waitForTerminal();
|
||||
await waitForXtreamRendererTerminal(options.app.mainWindow);
|
||||
const rendererProbe = await stopXtreamRendererProbe(
|
||||
options.app.mainWindow
|
||||
);
|
||||
rendererProbeInstalled = false;
|
||||
assertCompleteXtreamRendererProbe(rendererProbe);
|
||||
await assertXtreamSettlementAfterTerminal({
|
||||
captureGeneration,
|
||||
readStatus: () =>
|
||||
readXtreamMainCaptureStatus(options.app.electronApp),
|
||||
scenarioId: XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
terminal: driverTerminal,
|
||||
});
|
||||
const state = await options.control.state();
|
||||
await writeXtreamSafeJson(
|
||||
join(options.iterationDirectory, 'seed-control-state.json'),
|
||||
state,
|
||||
options.safety
|
||||
);
|
||||
assertXtreamControlStateForScenario(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
state,
|
||||
options.fixture
|
||||
);
|
||||
} finally {
|
||||
await Promise.allSettled([
|
||||
rendererProbeInstalled
|
||||
? stopXtreamRendererProbe(options.app.mainWindow)
|
||||
: Promise.resolve(),
|
||||
seed.dispose(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export interface AssertXtreamSettlementAfterTerminalOptions {
|
||||
readonly captureGeneration: number;
|
||||
readonly readStatus: () => Promise<XtreamMainCaptureStatus>;
|
||||
readonly scenarioId: XtreamScenarioId;
|
||||
readonly terminal: Promise<unknown>;
|
||||
}
|
||||
|
||||
export async function assertXtreamSettlementAfterTerminal(
|
||||
options: AssertXtreamSettlementAfterTerminalOptions
|
||||
): Promise<void> {
|
||||
await options.terminal;
|
||||
const status = await options.readStatus();
|
||||
if (status.captureGeneration !== options.captureGeneration) {
|
||||
throw new Error('xtream-measured-capture-generation-changed');
|
||||
}
|
||||
if (!isXtreamMainCaptureSettled(status, options.scenarioId)) {
|
||||
throw new Error('xtream-main-not-settled-after-terminal');
|
||||
}
|
||||
}
|
||||
|
||||
export interface FinalizeXtreamCaptureAfterRendererTerminalOptions<
|
||||
DriverTerminal,
|
||||
MainCapture,
|
||||
RendererCapture,
|
||||
> extends Omit<AssertXtreamSettlementAfterTerminalOptions, 'terminal'> {
|
||||
readonly driverTerminal: Promise<DriverTerminal>;
|
||||
readonly rendererTerminal: Promise<unknown>;
|
||||
readonly stopMain: () => Promise<MainCapture>;
|
||||
readonly stopRenderer: () => Promise<RendererCapture>;
|
||||
}
|
||||
|
||||
export interface FinalizedXtreamCapture<
|
||||
DriverTerminal,
|
||||
MainCapture,
|
||||
RendererCapture,
|
||||
> {
|
||||
readonly driverTerminal: DriverTerminal;
|
||||
readonly mainCapture: MainCapture;
|
||||
readonly rendererCapture: RendererCapture;
|
||||
}
|
||||
|
||||
export async function finalizeXtreamCaptureAfterRendererTerminal<
|
||||
DriverTerminal,
|
||||
MainCapture,
|
||||
RendererCapture,
|
||||
>(
|
||||
options: FinalizeXtreamCaptureAfterRendererTerminalOptions<
|
||||
DriverTerminal,
|
||||
MainCapture,
|
||||
RendererCapture
|
||||
>
|
||||
): Promise<
|
||||
FinalizedXtreamCapture<DriverTerminal, MainCapture, RendererCapture>
|
||||
> {
|
||||
await assertXtreamSettlementAfterTerminal({
|
||||
captureGeneration: options.captureGeneration,
|
||||
readStatus: options.readStatus,
|
||||
scenarioId: options.scenarioId,
|
||||
terminal: options.rendererTerminal,
|
||||
});
|
||||
const rendererCapture = options.stopRenderer();
|
||||
const mainCapture = options.stopMain();
|
||||
const [resolvedRenderer, resolvedMain, driverTerminal] = await Promise.all([
|
||||
rendererCapture,
|
||||
mainCapture,
|
||||
options.driverTerminal,
|
||||
]);
|
||||
return Object.freeze({
|
||||
driverTerminal,
|
||||
mainCapture: resolvedMain,
|
||||
rendererCapture: resolvedRenderer,
|
||||
});
|
||||
}
|
||||
|
||||
export async function assertXtreamBenchmarkEnvironmentIsolated(
|
||||
app: LaunchedElectronApp
|
||||
): Promise<void> {
|
||||
const keys = await app.electronApp.evaluate(() => Object.keys(process.env));
|
||||
const allowed = new Set(
|
||||
[
|
||||
...ELECTRON_RUNTIME_ENVIRONMENT_KEYS,
|
||||
'ELECTRON_IS_DEV',
|
||||
'IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS',
|
||||
'IPTVNATOR_DB_WORKER_BATCH_DELAY_MS',
|
||||
'IPTVNATOR_E2E_DATA_DIR',
|
||||
'IPTVNATOR_PERF_CAPTURE',
|
||||
'IPTVNATOR_PERF_WORKER_PROFILING',
|
||||
'IPTVNATOR_TRACE_RENDERER_CONSOLE',
|
||||
'NODE_ENV',
|
||||
].map((key) => key.toUpperCase())
|
||||
);
|
||||
if (
|
||||
!Array.isArray(keys) ||
|
||||
keys.some(
|
||||
(key) => typeof key !== 'string' || !allowed.has(key.toUpperCase())
|
||||
)
|
||||
) {
|
||||
throw new Error('xtream-benchmark-environment-reached-electron');
|
||||
}
|
||||
}
|
||||
|
||||
export function createXtreamSourceTitle(
|
||||
definition: XtreamIterationDefinition
|
||||
): string {
|
||||
return `Synthetic Xtream 100k ${definition.scenarioId} ${definition.runId}`;
|
||||
}
|
||||
|
||||
export async function createXtreamDiagnosticEvidence(
|
||||
kind: XtreamIterationDefinition['kind'],
|
||||
main: XtreamMainCaptureResult,
|
||||
renderer: XtreamRendererCaptureMetrics,
|
||||
iterationDirectory: string
|
||||
): Promise<XtreamDiagnosticArtifactEvidence | null> {
|
||||
if (kind !== 'diagnostic') return null;
|
||||
return validateXtreamDiagnosticArtifacts(
|
||||
{
|
||||
main: {
|
||||
cpuProfilePath: main.capture.cpuProfilePath,
|
||||
heapSnapshotPath: main.capture.heapSnapshotPath,
|
||||
workers: main.workers.map(
|
||||
({
|
||||
kind: workerKind,
|
||||
ordinal,
|
||||
profilePath,
|
||||
snapshotPath,
|
||||
}) => ({
|
||||
kind: workerKind,
|
||||
ordinal,
|
||||
profilePath,
|
||||
snapshotPath,
|
||||
})
|
||||
),
|
||||
},
|
||||
renderer: {
|
||||
cpuProfilePath: renderer.cpuProfilePath,
|
||||
heapSnapshotPath: renderer.heapSnapshotPath,
|
||||
tracePath: renderer.tracePath,
|
||||
},
|
||||
},
|
||||
iterationDirectory
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
closeElectronApp,
|
||||
launchElectronApp,
|
||||
type LaunchedElectronApp,
|
||||
} from '../electron-test-fixtures';
|
||||
import {
|
||||
type XtreamIterationDefinition,
|
||||
type XtreamPerformanceManifest,
|
||||
XTREAM_SCENARIO_ID,
|
||||
} from './xtream-benchmark-contract';
|
||||
import type { XtreamBenchmarkConfiguration } from './xtream-benchmark-configuration';
|
||||
import type { XtreamControlClient } from './xtream-control-client';
|
||||
import {
|
||||
assertXtreamControlReadyAfterObservationReset,
|
||||
assertXtreamControlStateForScenario,
|
||||
assertXtreamManifestIdentity,
|
||||
} from './xtream-control-client';
|
||||
import { createXtreamCatalogVerification } from './xtream-catalog-database-verification';
|
||||
import { readXtreamCatalogDatabaseSnapshot } from './xtream-catalog-database-reader';
|
||||
import { assembleXtreamRawIteration } from './xtream-iteration-assembler';
|
||||
import {
|
||||
beginXtreamMainMeasurement,
|
||||
installMainCapture,
|
||||
readXtreamMainCaptureStatus,
|
||||
rolloverXtreamMainCapture,
|
||||
startMainCapture,
|
||||
stopXtreamMainCapture,
|
||||
type MainCaptureStartOptions,
|
||||
} from './m3u-refresh-main-capture';
|
||||
import {
|
||||
assertRendererCdpTarget,
|
||||
assertTcpPortAvailable,
|
||||
resolveRendererWindowIdentity,
|
||||
} from './m3u-import-benchmark-support';
|
||||
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
|
||||
import {
|
||||
createXtreamArtifactSafetyContext,
|
||||
persistXtreamIteration,
|
||||
writeXtreamSafeJson,
|
||||
type PersistedXtreamIteration,
|
||||
} from './xtream-benchmark-artifacts';
|
||||
import {
|
||||
createXtreamSingleAttemptCapture,
|
||||
persistXtreamIterationFailureEvidenceAndThrow,
|
||||
type XtreamBenchmarkFailureStage,
|
||||
} from './xtream-benchmark-lifecycle';
|
||||
import {
|
||||
assertXtreamBenchmarkEnvironmentIsolated,
|
||||
createXtreamDiagnosticEvidence,
|
||||
createXtreamSourceTitle,
|
||||
finalizeXtreamCaptureAfterRendererTerminal,
|
||||
prearmXtreamDatabaseWorker,
|
||||
seedXtreamExistingPortal,
|
||||
} from './xtream-benchmark-iteration-support';
|
||||
import {
|
||||
deriveXtreamMeasuredPlaylistId,
|
||||
disableGenericElectronFixtureProbes,
|
||||
xtreamScenarioRequiresSeed,
|
||||
} from './xtream-benchmark-runtime';
|
||||
import {
|
||||
captureXtreamLaunchIdentity,
|
||||
createXtreamIterationProcessEvidence,
|
||||
} from './xtream-process-evidence';
|
||||
import {
|
||||
startXtreamRendererCapture,
|
||||
type RunningXtreamRendererCapture,
|
||||
} from './xtream-renderer-capture';
|
||||
import {
|
||||
prepareXtreamScenario,
|
||||
type PreparedXtreamScenario,
|
||||
type XtreamScenarioTerminalEvidence,
|
||||
type XtreamScenarioTriggerEvidence,
|
||||
} from './xtream-scenario-driver';
|
||||
import { runXtreamBackgroundUiActions } from './xtream-ui-action-probe';
|
||||
|
||||
export interface XtreamBenchmarkIterationContext {
|
||||
readonly configuration: XtreamBenchmarkConfiguration;
|
||||
readonly control: XtreamControlClient;
|
||||
readonly definition: XtreamIterationDefinition;
|
||||
readonly expectedFixture: XtreamPerformanceManifest;
|
||||
readonly scenarioDirectory: string;
|
||||
readonly seenElectronPids: Set<number>;
|
||||
}
|
||||
export async function runXtreamBenchmarkIteration(
|
||||
context: XtreamBenchmarkIterationContext
|
||||
): Promise<PersistedXtreamIteration> {
|
||||
const { configuration, control, definition, scenarioDirectory } = context;
|
||||
const iterationDirectory = join(scenarioDirectory, definition.runId);
|
||||
await assertPerformanceArtifactCapacity(iterationDirectory);
|
||||
await mkdir(iterationDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: false,
|
||||
});
|
||||
const dataDirectory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-xtream-performance-')
|
||||
);
|
||||
const safety = createXtreamArtifactSafetyContext(
|
||||
configuration.controlToken
|
||||
);
|
||||
let app: LaunchedElectronApp | null = null;
|
||||
let renderer: RunningXtreamRendererCapture | null = null;
|
||||
let prepared: PreparedXtreamScenario | null = null;
|
||||
let controlState: Awaited<ReturnType<XtreamControlClient['state']>> | null =
|
||||
null;
|
||||
let failureStage: XtreamBenchmarkFailureStage = 'capture-setup';
|
||||
let stopMainCaptureOnce:
|
||||
(() => ReturnType<typeof stopXtreamMainCapture>) | null = null;
|
||||
let stopRendererCaptureOnce: RunningXtreamRendererCapture['stop'] | null =
|
||||
null;
|
||||
let terminal: XtreamScenarioTerminalEvidence | null = null;
|
||||
let trigger: XtreamScenarioTriggerEvidence | null = null;
|
||||
try {
|
||||
await control.resetAll();
|
||||
const fixture = await control.prepare();
|
||||
assertXtreamManifestIdentity(context.expectedFixture, fixture);
|
||||
await assertTcpPortAvailable(configuration.cdpPort);
|
||||
app = await launchElectronApp(dataDirectory, {
|
||||
args: [
|
||||
'--js-flags=--expose-gc',
|
||||
'--remote-debugging-address=127.0.0.1',
|
||||
`--remote-debugging-port=${configuration.cdpPort}`,
|
||||
`--user-data-dir=${join(dataDirectory, 'user-data')}`,
|
||||
],
|
||||
environmentInheritance: 'runtime-only',
|
||||
env: {
|
||||
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
|
||||
IPTVNATOR_PERF_CAPTURE: '1',
|
||||
IPTVNATOR_PERF_WORKER_PROFILING: '1',
|
||||
IPTVNATOR_TRACE_RENDERER_CONSOLE: '0',
|
||||
},
|
||||
omitEnvKeys: [
|
||||
'IPTVNATOR_XTREAM_MOCK_CONTROL',
|
||||
'IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN',
|
||||
],
|
||||
});
|
||||
app.mainWindow.setDefaultTimeout(10 * 60 * 1_000);
|
||||
await assertXtreamBenchmarkEnvironmentIsolated(app);
|
||||
await disableGenericElectronFixtureProbes(app.mainWindow);
|
||||
await assertRendererCdpTarget(app.mainWindow, configuration.cdpPort);
|
||||
await installMainCapture(app.electronApp);
|
||||
await prearmXtreamDatabaseWorker(app);
|
||||
const electronApp = app.electronApp;
|
||||
const launch = await captureXtreamLaunchIdentity(
|
||||
app.electronApp,
|
||||
app.electronApp.process().pid,
|
||||
context.seenElectronPids
|
||||
);
|
||||
const rendererWindowIdentity = await resolveRendererWindowIdentity(app);
|
||||
const captureOptions: MainCaptureStartOptions = {
|
||||
deferMeasurement: true,
|
||||
diagnostic: definition.kind === 'diagnostic',
|
||||
outputDirectory: iterationDirectory,
|
||||
rendererWindowIdentity,
|
||||
};
|
||||
const sourceTitle = createXtreamSourceTitle(definition);
|
||||
const seeded = xtreamScenarioRequiresSeed(definition.scenarioId);
|
||||
if (seeded) {
|
||||
failureStage = 'seed';
|
||||
stopMainCaptureOnce = createXtreamSingleAttemptCapture(() =>
|
||||
stopXtreamMainCapture(electronApp)
|
||||
);
|
||||
await seedXtreamExistingPortal({
|
||||
app,
|
||||
captureOptions,
|
||||
control,
|
||||
fixture,
|
||||
iterationDirectory,
|
||||
safety,
|
||||
serverUrl: configuration.mockOrigin,
|
||||
sourceTitle,
|
||||
});
|
||||
}
|
||||
failureStage = 'scenario-preparation';
|
||||
prepared = await prepareXtreamScenario(app.mainWindow, {
|
||||
scenarioId: definition.scenarioId,
|
||||
serverUrl: configuration.mockOrigin,
|
||||
sourceTitle,
|
||||
});
|
||||
await control.resetObservations();
|
||||
assertXtreamControlReadyAfterObservationReset(
|
||||
await control.state(),
|
||||
fixture
|
||||
);
|
||||
let captureGeneration: number;
|
||||
failureStage = 'measured-capture-start';
|
||||
if (seeded) {
|
||||
const rolloverOnce = createXtreamSingleAttemptCapture(() =>
|
||||
rolloverXtreamMainCapture(electronApp, captureOptions)
|
||||
);
|
||||
stopMainCaptureOnce = rolloverOnce;
|
||||
const rollover = await rolloverOnce();
|
||||
stopMainCaptureOnce = createXtreamSingleAttemptCapture(() =>
|
||||
stopXtreamMainCapture(electronApp)
|
||||
);
|
||||
if (
|
||||
!rollover.rollover?.nextCaptureStarted ||
|
||||
rollover.rollover.nextCaptureUnavailableReason !== null
|
||||
) {
|
||||
throw new Error('xtream-measured-capture-rollover-failed');
|
||||
}
|
||||
await writeXtreamSafeJson(
|
||||
join(iterationDirectory, 'seed-main-capture.json'),
|
||||
rollover,
|
||||
safety
|
||||
);
|
||||
captureGeneration = rollover.captureGeneration + 1;
|
||||
if (
|
||||
(await readXtreamMainCaptureStatus(app.electronApp))
|
||||
.captureGeneration !== captureGeneration
|
||||
) {
|
||||
throw new Error('xtream-measured-capture-generation-changed');
|
||||
}
|
||||
} else {
|
||||
stopMainCaptureOnce = createXtreamSingleAttemptCapture(() =>
|
||||
stopXtreamMainCapture(electronApp)
|
||||
);
|
||||
await startMainCapture(app.electronApp, captureOptions);
|
||||
captureGeneration = (
|
||||
await readXtreamMainCaptureStatus(app.electronApp)
|
||||
).captureGeneration;
|
||||
}
|
||||
failureStage = 'renderer-capture-start';
|
||||
renderer = await startXtreamRendererCapture(app.mainWindow, {
|
||||
diagnostic: definition.kind === 'diagnostic',
|
||||
outputDirectory: iterationDirectory,
|
||||
scenarioId: definition.scenarioId,
|
||||
sourceTitle,
|
||||
});
|
||||
const runningRenderer = renderer;
|
||||
stopRendererCaptureOnce = createXtreamSingleAttemptCapture(() =>
|
||||
runningRenderer.stop()
|
||||
);
|
||||
await beginXtreamMainMeasurement(app.electronApp);
|
||||
await renderer.beginOperation();
|
||||
failureStage = 'operation-trigger';
|
||||
trigger = await prepared.trigger();
|
||||
if (definition.scenarioId === XTREAM_SCENARIO_ID.BACKGROUND_UI) {
|
||||
failureStage = 'background-ui';
|
||||
await prepared.runBackgroundProbe(
|
||||
({ operationId, page, playlistId }) =>
|
||||
runXtreamBackgroundUiActions(page, {
|
||||
operationId,
|
||||
playlistId,
|
||||
searchTerm: 'Performance',
|
||||
})
|
||||
);
|
||||
}
|
||||
failureStage = 'terminal-settlement';
|
||||
const terminalPromise = prepared.waitForTerminal().then((value) => {
|
||||
terminal = value;
|
||||
return value;
|
||||
});
|
||||
if (!stopRendererCaptureOnce || !stopMainCaptureOnce) {
|
||||
throw new Error('xtream-measured-capture-lifecycle-invalid');
|
||||
}
|
||||
const finalized = await finalizeXtreamCaptureAfterRendererTerminal({
|
||||
captureGeneration,
|
||||
driverTerminal: terminalPromise,
|
||||
readStatus: () => readXtreamMainCaptureStatus(app.electronApp),
|
||||
rendererTerminal: renderer.waitForTerminal(),
|
||||
scenarioId: definition.scenarioId,
|
||||
stopMain: stopMainCaptureOnce,
|
||||
stopRenderer: stopRendererCaptureOnce,
|
||||
});
|
||||
terminal = finalized.driverTerminal;
|
||||
const { mainCapture, rendererCapture } = finalized;
|
||||
if (!terminal) {
|
||||
throw new Error('xtream-measured-capture-lifecycle-invalid');
|
||||
}
|
||||
if (mainCapture.captureGeneration !== captureGeneration) {
|
||||
throw new Error('xtream-measured-capture-generation-changed');
|
||||
}
|
||||
controlState = await control.state();
|
||||
failureStage = 'evidence-validation';
|
||||
await Promise.all([
|
||||
writeXtreamSafeJson(
|
||||
join(iterationDirectory, 'main-capture.json'),
|
||||
mainCapture,
|
||||
safety
|
||||
),
|
||||
writeXtreamSafeJson(
|
||||
join(iterationDirectory, 'renderer-capture.json'),
|
||||
rendererCapture,
|
||||
safety
|
||||
),
|
||||
writeXtreamSafeJson(
|
||||
join(iterationDirectory, 'terminal-evidence.json'),
|
||||
terminal,
|
||||
safety
|
||||
),
|
||||
writeXtreamSafeJson(
|
||||
join(iterationDirectory, 'trigger-evidence.json'),
|
||||
trigger,
|
||||
safety
|
||||
),
|
||||
writeXtreamSafeJson(
|
||||
join(iterationDirectory, 'control-state.json'),
|
||||
controlState,
|
||||
safety
|
||||
),
|
||||
]);
|
||||
assertXtreamControlStateForScenario(
|
||||
definition.scenarioId,
|
||||
controlState,
|
||||
fixture
|
||||
);
|
||||
const playlistId = deriveXtreamMeasuredPlaylistId(mainCapture.requests);
|
||||
const catalogSnapshot = await readXtreamCatalogDatabaseSnapshot(
|
||||
app.electronApp,
|
||||
{
|
||||
captureStoppedEpochMs: mainCapture.captureStoppedEpochMs,
|
||||
dataDirectory,
|
||||
playlistId,
|
||||
requireFromPath: join(
|
||||
configuration.workspaceRoot,
|
||||
'dist/apps/electron-backend/main.js'
|
||||
),
|
||||
}
|
||||
);
|
||||
await writeXtreamSafeJson(
|
||||
join(iterationDirectory, 'catalog-snapshot.json'),
|
||||
catalogSnapshot,
|
||||
safety
|
||||
);
|
||||
const catalogVerification = createXtreamCatalogVerification(
|
||||
catalogSnapshot,
|
||||
fixture,
|
||||
definition.scenarioId
|
||||
);
|
||||
const diagnosticEvidence = await createXtreamDiagnosticEvidence(
|
||||
definition.kind,
|
||||
mainCapture,
|
||||
rendererCapture,
|
||||
iterationDirectory
|
||||
);
|
||||
const processEvidence = await createXtreamIterationProcessEvidence({
|
||||
...launch,
|
||||
diagnosticDirectorySha256:
|
||||
diagnosticEvidence?.directorySha256 ?? null,
|
||||
iterationDirectory,
|
||||
});
|
||||
const raw = assembleXtreamRawIteration({
|
||||
catalogVerification,
|
||||
definition,
|
||||
diagnosticArtifacts: diagnosticEvidence,
|
||||
mainCapture,
|
||||
process: processEvidence,
|
||||
rendererCapture,
|
||||
terminal,
|
||||
trigger,
|
||||
});
|
||||
failureStage = 'result-persistence';
|
||||
return persistXtreamIteration(
|
||||
iterationDirectory,
|
||||
raw,
|
||||
diagnosticEvidence,
|
||||
safety
|
||||
);
|
||||
} catch (failure) {
|
||||
const electronApp = app?.electronApp;
|
||||
await persistXtreamIterationFailureEvidenceAndThrow(failure, {
|
||||
evidence: {
|
||||
control: async () => controlState ?? control.state(),
|
||||
...(electronApp
|
||||
? {
|
||||
mainStatus: () =>
|
||||
readXtreamMainCaptureStatus(electronApp),
|
||||
}
|
||||
: {}),
|
||||
...(stopMainCaptureOnce
|
||||
? { mainCapture: stopMainCaptureOnce }
|
||||
: {}),
|
||||
...(stopRendererCaptureOnce
|
||||
? { rendererCapture: stopRendererCaptureOnce }
|
||||
: {}),
|
||||
...(terminal ? { terminal: async () => terminal } : {}),
|
||||
...(trigger ? { trigger: async () => trigger } : {}),
|
||||
},
|
||||
persist: (filename, value) =>
|
||||
writeXtreamSafeJson(
|
||||
join(iterationDirectory, filename),
|
||||
value,
|
||||
safety
|
||||
),
|
||||
stage: failureStage,
|
||||
});
|
||||
} finally {
|
||||
await prepared?.dispose().catch(() => undefined);
|
||||
await renderer?.dispose().catch(() => undefined);
|
||||
if (app) await closeElectronApp(app);
|
||||
await rm(dataDirectory, { force: true, recursive: true }).catch(
|
||||
() => undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
createXtreamSingleAttemptCapture,
|
||||
persistXtreamIterationFailureEvidence,
|
||||
persistXtreamIterationFailureEvidenceAndThrow,
|
||||
} from './xtream-benchmark-lifecycle';
|
||||
|
||||
const performanceDirectory = resolve(process.cwd(), 'src/performance');
|
||||
|
||||
describe('Xtream benchmark failure lifecycle', () => {
|
||||
it('memoizes capture finalization across success and failure paths', async () => {
|
||||
let attempts = 0;
|
||||
const capture = createXtreamSingleAttemptCapture(async () => {
|
||||
attempts += 1;
|
||||
return Object.freeze({ capture: 'complete' });
|
||||
});
|
||||
|
||||
const [first, second] = await Promise.all([capture(), capture()]);
|
||||
const third = await capture();
|
||||
|
||||
assert.equal(attempts, 1);
|
||||
assert.strictEqual(first, second);
|
||||
assert.strictEqual(second, third);
|
||||
|
||||
let failedAttempts = 0;
|
||||
const failedCapture = createXtreamSingleAttemptCapture(async () => {
|
||||
failedAttempts += 1;
|
||||
throw new Error('fixed-test-failure');
|
||||
});
|
||||
await assert.rejects(failedCapture, /fixed-test-failure/);
|
||||
await assert.rejects(failedCapture, /fixed-test-failure/);
|
||||
assert.equal(failedAttempts, 1);
|
||||
});
|
||||
|
||||
it('persists all available evidence and only fixed failure metadata', async () => {
|
||||
const persisted = new Map<string, unknown>();
|
||||
const attempts: string[] = [];
|
||||
|
||||
await persistXtreamIterationFailureEvidence({
|
||||
evidence: {
|
||||
control: async () => {
|
||||
attempts.push('control');
|
||||
return { epoch: 11 };
|
||||
},
|
||||
mainCapture: async () => {
|
||||
attempts.push('mainCapture');
|
||||
return { captureGeneration: 3 };
|
||||
},
|
||||
mainStatus: async () => {
|
||||
attempts.push('mainStatus');
|
||||
return { captureGeneration: 3, pending: 1 };
|
||||
},
|
||||
rendererCapture: async () => {
|
||||
attempts.push('rendererCapture');
|
||||
throw new Error(
|
||||
'https://performance:performance@example.invalid?token=secret'
|
||||
);
|
||||
},
|
||||
terminal: async () => {
|
||||
attempts.push('terminal');
|
||||
return { terminalEpochMs: 42 };
|
||||
},
|
||||
},
|
||||
persist: async (filename, value) => {
|
||||
persisted.set(filename, value);
|
||||
},
|
||||
stage: 'terminal-settlement',
|
||||
});
|
||||
|
||||
assert.deepEqual(attempts.sort(), [
|
||||
'control',
|
||||
'mainCapture',
|
||||
'mainStatus',
|
||||
'rendererCapture',
|
||||
'terminal',
|
||||
]);
|
||||
assert.deepEqual([...persisted.keys()].sort(), [
|
||||
'failure-control-state.json',
|
||||
'failure-main-capture.json',
|
||||
'failure-main-status.json',
|
||||
'failure-terminal-evidence.json',
|
||||
'failure.json',
|
||||
]);
|
||||
const metadata = persisted.get('failure.json');
|
||||
assert.deepEqual(metadata, {
|
||||
evidence: {
|
||||
control: 'captured',
|
||||
mainCapture: 'captured',
|
||||
mainStatus: 'captured',
|
||||
rendererCapture: 'capture-failed',
|
||||
terminal: 'captured',
|
||||
trigger: 'not-available',
|
||||
},
|
||||
reason: 'xtream-benchmark-iteration-failed',
|
||||
stage: 'terminal-settlement',
|
||||
status: 'failed',
|
||||
validForComparison: false,
|
||||
});
|
||||
const serialized = JSON.stringify([...persisted.values()]);
|
||||
assert.doesNotMatch(serialized, /performance:performance|secret/iu);
|
||||
assert.doesNotMatch(serialized, /message|stack/iu);
|
||||
});
|
||||
|
||||
it('persists fixed failure metadata when capture setup never completed', async () => {
|
||||
const persisted = new Map<string, unknown>();
|
||||
|
||||
await persistXtreamIterationFailureEvidence({
|
||||
evidence: {},
|
||||
persist: async (filename, value) => {
|
||||
persisted.set(filename, value);
|
||||
},
|
||||
stage: 'capture-setup',
|
||||
});
|
||||
|
||||
assert.deepEqual([...persisted.keys()], ['failure.json']);
|
||||
assert.deepEqual(persisted.get('failure.json'), {
|
||||
evidence: {
|
||||
control: 'not-available',
|
||||
mainCapture: 'not-available',
|
||||
mainStatus: 'not-available',
|
||||
rendererCapture: 'not-available',
|
||||
terminal: 'not-available',
|
||||
trigger: 'not-available',
|
||||
},
|
||||
reason: 'xtream-benchmark-iteration-failed',
|
||||
stage: 'capture-setup',
|
||||
status: 'failed',
|
||||
validForComparison: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('fails when aggregate failure evidence cannot be persisted', async () => {
|
||||
await assert.rejects(
|
||||
persistXtreamIterationFailureEvidence({
|
||||
evidence: {},
|
||||
persist: async (filename) => {
|
||||
if (filename === 'failure.json') {
|
||||
throw new Error('fixed-persistence-failure');
|
||||
}
|
||||
},
|
||||
stage: 'capture-setup',
|
||||
}),
|
||||
/fixed-persistence-failure/
|
||||
);
|
||||
});
|
||||
|
||||
it('rethrows the original iteration failure after evidence persistence succeeds', async () => {
|
||||
const originalFailure = new Error('fixed-original-failure');
|
||||
|
||||
await assert.rejects(
|
||||
persistXtreamIterationFailureEvidenceAndThrow(originalFailure, {
|
||||
evidence: {},
|
||||
persist: async () => undefined,
|
||||
stage: 'capture-setup',
|
||||
}),
|
||||
(failure: unknown) => {
|
||||
assert.strictEqual(failure, originalFailure);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves both the original and evidence persistence failures', async () => {
|
||||
const originalFailure = new Error('fixed-original-failure');
|
||||
const persistenceFailure = new Error('fixed-persistence-failure');
|
||||
|
||||
await assert.rejects(
|
||||
persistXtreamIterationFailureEvidenceAndThrow(originalFailure, {
|
||||
evidence: {},
|
||||
persist: async (filename) => {
|
||||
if (filename === 'failure.json') {
|
||||
throw persistenceFailure;
|
||||
}
|
||||
},
|
||||
stage: 'capture-setup',
|
||||
}),
|
||||
(failure: unknown) => {
|
||||
assert.ok(failure instanceof AggregateError);
|
||||
assert.deepEqual(failure.errors, [
|
||||
originalFailure,
|
||||
persistenceFailure,
|
||||
]);
|
||||
assert.strictEqual(failure.cause, originalFailure);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('collects failure evidence before teardown and writes summary before its guard', () => {
|
||||
const iteration = readFileSync(
|
||||
resolve(performanceDirectory, 'xtream-benchmark-iteration.ts'),
|
||||
'utf8'
|
||||
);
|
||||
const failureCatch = iteration.indexOf('} catch (failure) {');
|
||||
const failurePersistence = iteration.indexOf(
|
||||
'persistXtreamIterationFailureEvidenceAndThrow(',
|
||||
failureCatch
|
||||
);
|
||||
const teardown = iteration.indexOf('} finally {', failureCatch);
|
||||
|
||||
assert.ok(failureCatch >= 0, 'iteration failure catch is missing');
|
||||
assert.ok(
|
||||
failurePersistence > failureCatch,
|
||||
'failure evidence persistence is missing'
|
||||
);
|
||||
assert.ok(
|
||||
teardown > failurePersistence,
|
||||
'teardown occurs before failure evidence persistence'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
iteration.slice(failureCatch, teardown),
|
||||
/if \(captureInstalled\)/u,
|
||||
'early capture-setup failure metadata is incorrectly gated'
|
||||
);
|
||||
assert.match(iteration, /createXtreamSingleAttemptCapture/);
|
||||
const seededBranch = iteration.indexOf('if (seeded) {');
|
||||
const seedStop = iteration.indexOf(
|
||||
'stopMainCaptureOnce = createXtreamSingleAttemptCapture',
|
||||
seededBranch
|
||||
);
|
||||
const seedRun = iteration.indexOf(
|
||||
'await seedXtreamExistingPortal',
|
||||
seededBranch
|
||||
);
|
||||
assert.ok(
|
||||
seedStop > seededBranch && seedRun > seedStop,
|
||||
'seed capture does not have failure finalization armed'
|
||||
);
|
||||
|
||||
const orchestrator = readFileSync(
|
||||
resolve(performanceDirectory, 'xtream-benchmark-orchestrator.ts'),
|
||||
'utf8'
|
||||
);
|
||||
const summaryWrite = orchestrator.indexOf(
|
||||
"join(configuration.outputDirectory, 'summary.json')"
|
||||
);
|
||||
const validityGuard = orchestrator.indexOf(
|
||||
'summary.validForComparison !=='
|
||||
);
|
||||
assert.ok(summaryWrite >= 0, 'summary persistence is missing');
|
||||
assert.ok(
|
||||
validityGuard > summaryWrite,
|
||||
'comparison-validity guard runs before summary persistence'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
export type XtreamBenchmarkFailureStage =
|
||||
| 'background-ui'
|
||||
| 'capture-setup'
|
||||
| 'capture-stop'
|
||||
| 'evidence-validation'
|
||||
| 'measured-capture-start'
|
||||
| 'operation-trigger'
|
||||
| 'renderer-capture-start'
|
||||
| 'result-persistence'
|
||||
| 'scenario-preparation'
|
||||
| 'seed'
|
||||
| 'terminal-settlement';
|
||||
|
||||
type FailureEvidenceName =
|
||||
| 'control'
|
||||
| 'mainCapture'
|
||||
| 'mainStatus'
|
||||
| 'rendererCapture'
|
||||
| 'terminal'
|
||||
| 'trigger';
|
||||
|
||||
type FailureEvidenceStatus = 'capture-failed' | 'captured' | 'not-available';
|
||||
|
||||
interface PersistXtreamIterationFailureEvidenceOptions {
|
||||
readonly evidence: Partial<
|
||||
Record<FailureEvidenceName, () => Promise<unknown>>
|
||||
>;
|
||||
readonly persist: (filename: string, value: unknown) => Promise<void>;
|
||||
readonly stage: XtreamBenchmarkFailureStage;
|
||||
}
|
||||
|
||||
const EVIDENCE_ARTIFACTS: readonly [FailureEvidenceName, string][] =
|
||||
Object.freeze([
|
||||
['mainStatus', 'failure-main-status.json'],
|
||||
['mainCapture', 'failure-main-capture.json'],
|
||||
['rendererCapture', 'failure-renderer-capture.json'],
|
||||
['trigger', 'failure-trigger-evidence.json'],
|
||||
['terminal', 'failure-terminal-evidence.json'],
|
||||
['control', 'failure-control-state.json'],
|
||||
]);
|
||||
|
||||
export function createXtreamSingleAttemptCapture<T>(
|
||||
capture: () => Promise<T>
|
||||
): () => Promise<T> {
|
||||
let attempt: Promise<T> | null = null;
|
||||
return () => {
|
||||
attempt ??= Promise.resolve().then(capture);
|
||||
return attempt;
|
||||
};
|
||||
}
|
||||
|
||||
export async function persistXtreamIterationFailureEvidence(
|
||||
options: PersistXtreamIterationFailureEvidenceOptions
|
||||
): Promise<void> {
|
||||
const status = {} as Record<FailureEvidenceName, FailureEvidenceStatus>;
|
||||
for (const [name, filename] of EVIDENCE_ARTIFACTS) {
|
||||
const capture = options.evidence[name];
|
||||
if (!capture) {
|
||||
status[name] = 'not-available';
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const value = await capture();
|
||||
await options.persist(filename, value);
|
||||
status[name] = 'captured';
|
||||
} catch {
|
||||
status[name] = 'capture-failed';
|
||||
}
|
||||
}
|
||||
await options.persist('failure.json', {
|
||||
evidence: status,
|
||||
reason: 'xtream-benchmark-iteration-failed',
|
||||
stage: options.stage,
|
||||
status: 'failed',
|
||||
validForComparison: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function persistXtreamIterationFailureEvidenceAndThrow(
|
||||
failure: unknown,
|
||||
options: PersistXtreamIterationFailureEvidenceOptions
|
||||
): Promise<never> {
|
||||
try {
|
||||
await persistXtreamIterationFailureEvidence(options);
|
||||
} catch (persistenceFailure) {
|
||||
throw new AggregateError(
|
||||
[failure, persistenceFailure],
|
||||
'Xtream benchmark iteration and failure evidence persistence failed',
|
||||
{ cause: failure }
|
||||
);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import type { XtreamBenchmarkManifest } from './xtream-benchmark-contract';
|
||||
import { parseXtreamBenchmarkBuildIdentity } from './xtream-build-identity-schema';
|
||||
import { assertXtreamArtifactRedacted } from './xtream-diagnostic-artifacts';
|
||||
import { XTREAM_MEMORY_ACCOUNTING } from './xtream-summary-contract';
|
||||
|
||||
export const XTREAM_SHA256 = /^[a-f0-9]{64}$/u;
|
||||
export const XTREAM_IDENTIFIER =
|
||||
/^(?!(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$))[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9_-])?$/iu;
|
||||
|
||||
const ROOT_KEYS = [
|
||||
'build',
|
||||
'environment',
|
||||
'fixture',
|
||||
'memoryAccounting',
|
||||
'mode',
|
||||
'runtime',
|
||||
'source',
|
||||
'suite',
|
||||
'variant',
|
||||
] as const;
|
||||
const ENVIRONMENT_KEYS = [
|
||||
'cdpAddress',
|
||||
'cdpPort',
|
||||
'databaseWorkerBatchDelayMs',
|
||||
'exposeGc',
|
||||
'freshUserDataPerIteration',
|
||||
'perfCapture',
|
||||
'perfWorkerProfiling',
|
||||
'traceRendererConsole',
|
||||
] as const;
|
||||
const FIXTURE_KEYS = [
|
||||
'bytes',
|
||||
'catalogSha256',
|
||||
'categories',
|
||||
'items',
|
||||
'scenario',
|
||||
'seed',
|
||||
] as const;
|
||||
const RUNTIME_KEYS = [
|
||||
'arch',
|
||||
'electronVersion',
|
||||
'harnessNodeVersion',
|
||||
'platform',
|
||||
] as const;
|
||||
const SOURCE_KEYS = ['gitCommit', 'gitDiffSha256', 'gitDirty'] as const;
|
||||
const MEMORY_KEYS = Object.keys(XTREAM_MEMORY_ACCOUNTING);
|
||||
|
||||
export function parseXtreamBenchmarkManifest(
|
||||
value: unknown
|
||||
): XtreamBenchmarkManifest {
|
||||
try {
|
||||
const input = snapshotExactRecord(value, ROOT_KEYS);
|
||||
const build = parseXtreamBenchmarkBuildIdentity(input['build']);
|
||||
const environment = snapshotExactRecord(
|
||||
input['environment'],
|
||||
ENVIRONMENT_KEYS
|
||||
);
|
||||
const fixture = snapshotExactRecord(input['fixture'], FIXTURE_KEYS);
|
||||
const memoryAccounting = snapshotExactRecord(
|
||||
input['memoryAccounting'],
|
||||
MEMORY_KEYS
|
||||
);
|
||||
const runtime = snapshotExactRecord(input['runtime'], RUNTIME_KEYS);
|
||||
const source = snapshotExactRecord(input['source'], SOURCE_KEYS);
|
||||
const snapshot = {
|
||||
build,
|
||||
environment: { ...environment },
|
||||
fixture: { ...fixture },
|
||||
memoryAccounting: { ...memoryAccounting },
|
||||
mode: input['mode'],
|
||||
runtime: { ...runtime },
|
||||
source: { ...source },
|
||||
suite: input['suite'],
|
||||
variant: input['variant'],
|
||||
};
|
||||
assertXtreamArtifactRedacted(snapshot);
|
||||
if (!isValidManifest(snapshot)) invalid();
|
||||
return freezeDeep(snapshot) as unknown as XtreamBenchmarkManifest;
|
||||
} catch {
|
||||
throw new Error('invalid Xtream benchmark manifest');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertXtreamBenchmarkManifest(
|
||||
value: unknown
|
||||
): asserts value is XtreamBenchmarkManifest {
|
||||
parseXtreamBenchmarkManifest(value);
|
||||
}
|
||||
|
||||
function isValidManifest(
|
||||
manifest: Record<string, unknown> & {
|
||||
environment: Record<string, unknown>;
|
||||
fixture: Record<string, unknown>;
|
||||
memoryAccounting: Record<string, unknown>;
|
||||
runtime: Record<string, unknown>;
|
||||
source: Record<string, unknown>;
|
||||
}
|
||||
): boolean {
|
||||
const { environment, fixture, memoryAccounting, runtime, source } =
|
||||
manifest;
|
||||
return (
|
||||
environment['cdpAddress'] === '127.0.0.1' &&
|
||||
isValidXtreamCdpPort(manifest['mode'], environment['cdpPort']) &&
|
||||
environment['databaseWorkerBatchDelayMs'] === 0 &&
|
||||
environment['exposeGc'] === true &&
|
||||
environment['freshUserDataPerIteration'] === true &&
|
||||
environment['perfCapture'] === '1' &&
|
||||
environment['perfWorkerProfiling'] === '1' &&
|
||||
environment['traceRendererConsole'] === '0' &&
|
||||
fixture['scenario'] === 'performance-100k' &&
|
||||
fixture['seed'] === 91_001 &&
|
||||
fixture['categories'] === 100 &&
|
||||
fixture['items'] === 100_000 &&
|
||||
isPositiveSafeInteger(fixture['bytes']) &&
|
||||
typeof fixture['catalogSha256'] === 'string' &&
|
||||
XTREAM_SHA256.test(fixture['catalogSha256']) &&
|
||||
Object.values(runtime).every(
|
||||
(entry) => typeof entry === 'string' && entry.length > 0
|
||||
) &&
|
||||
typeof source['gitCommit'] === 'string' &&
|
||||
/^[a-f0-9]{40}$/u.test(source['gitCommit']) &&
|
||||
typeof source['gitDiffSha256'] === 'string' &&
|
||||
XTREAM_SHA256.test(source['gitDiffSha256']) &&
|
||||
typeof source['gitDirty'] === 'boolean' &&
|
||||
(manifest['mode'] !== 'formal' || !source['gitDirty']) &&
|
||||
(manifest['mode'] === 'formal' || manifest['mode'] === 'smoke') &&
|
||||
manifest['suite'] === 'xtream-performance-100k' &&
|
||||
typeof manifest['variant'] === 'string' &&
|
||||
XTREAM_IDENTIFIER.test(manifest['variant']) &&
|
||||
MEMORY_KEYS.every(
|
||||
(key) =>
|
||||
memoryAccounting[key] ===
|
||||
XTREAM_MEMORY_ACCOUNTING[
|
||||
key as keyof typeof XTREAM_MEMORY_ACCOUNTING
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isValidXtreamCdpPort(mode: unknown, cdpPort: unknown): boolean {
|
||||
return (
|
||||
(mode === 'formal' || mode === 'smoke') &&
|
||||
typeof cdpPort === 'number' &&
|
||||
Number.isInteger(cdpPort) &&
|
||||
cdpPort >= 1024 &&
|
||||
cdpPort <= 65_535 &&
|
||||
(mode === 'smoke' || cdpPort === 9222)
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotExactRecord(
|
||||
value: unknown,
|
||||
expectedKeys: readonly string[]
|
||||
): Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
invalid();
|
||||
}
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expectedKeys.length ||
|
||||
keys.some(
|
||||
(key) => typeof key !== 'string' || !expectedKeys.includes(key)
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.fromEntries(
|
||||
expectedKeys.map((key) => [key, Reflect.get(value, key)])
|
||||
);
|
||||
}
|
||||
|
||||
function freezeDeep<T>(value: T): T {
|
||||
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
||||
Object.values(value).forEach(freezeDeep);
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isPositiveSafeInteger(value: unknown): boolean {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid manifest');
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
createXtreamIterationDefinitions,
|
||||
listXtreamBenchmarkScenarios,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
assertXtreamBenchmarkBuildUnchanged,
|
||||
createXtreamBenchmarkManifest,
|
||||
resolveXtreamBenchmarkConfiguration,
|
||||
} from './xtream-benchmark-configuration';
|
||||
import {
|
||||
createXtreamArtifactSafetyContext,
|
||||
writeXtreamSafeJson,
|
||||
} from './xtream-benchmark-artifacts';
|
||||
import { runXtreamBenchmarkIteration } from './xtream-benchmark-iteration';
|
||||
import { createXtreamBenchmarkSummary } from './xtream-benchmark-report';
|
||||
import {
|
||||
assertXtreamControlReadyAfterObservationReset,
|
||||
createXtreamControlClient,
|
||||
} from './xtream-control-client';
|
||||
import type { XtreamDiagnosticArtifactEvidence } from './xtream-diagnostic-artifacts';
|
||||
import type { XtreamValidatedIterationResult } from './xtream-summary-contract';
|
||||
|
||||
export async function runXtreamBenchmark(): Promise<void> {
|
||||
const configuration = await resolveXtreamBenchmarkConfiguration();
|
||||
const safety = createXtreamArtifactSafetyContext(
|
||||
configuration.controlToken
|
||||
);
|
||||
const control = createXtreamControlClient({
|
||||
origin: configuration.mockOrigin,
|
||||
token: configuration.controlToken,
|
||||
});
|
||||
const iterations: XtreamValidatedIterationResult[] = [];
|
||||
const diagnosticEvidence = new Map<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
>();
|
||||
const seenElectronPids = new Set<number>();
|
||||
try {
|
||||
await control.resetAll();
|
||||
const expectedFixture = await control.prepare();
|
||||
await control.resetObservations();
|
||||
assertXtreamControlReadyAfterObservationReset(
|
||||
await control.state(),
|
||||
expectedFixture
|
||||
);
|
||||
const manifest = createXtreamBenchmarkManifest(
|
||||
configuration,
|
||||
expectedFixture
|
||||
);
|
||||
await writeXtreamSafeJson(
|
||||
join(configuration.outputDirectory, 'manifest.json'),
|
||||
manifest,
|
||||
safety
|
||||
);
|
||||
const definitions = createXtreamIterationDefinitions(
|
||||
configuration.smoke
|
||||
);
|
||||
for (const scenario of listXtreamBenchmarkScenarios()) {
|
||||
const scenarioDirectory = join(
|
||||
configuration.outputDirectory,
|
||||
scenario.id
|
||||
);
|
||||
await mkdir(scenarioDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: false,
|
||||
});
|
||||
for (const definition of definitions.filter(
|
||||
({ scenarioId }) => scenarioId === scenario.id
|
||||
)) {
|
||||
console.log(
|
||||
`[performance] ${configuration.variant}/${scenario.id}/${definition.runId} starting`
|
||||
);
|
||||
const persisted = await runXtreamBenchmarkIteration({
|
||||
configuration,
|
||||
control,
|
||||
definition,
|
||||
expectedFixture,
|
||||
scenarioDirectory,
|
||||
seenElectronPids,
|
||||
});
|
||||
iterations.push(persisted.iteration);
|
||||
if (persisted.diagnosticEvidence) {
|
||||
diagnosticEvidence.set(
|
||||
persisted.iteration,
|
||||
persisted.diagnosticEvidence
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`[performance] ${scenario.id}/${definition.runId} total=${persisted.iteration.phases.totalMs.toFixed(3)}ms`
|
||||
);
|
||||
}
|
||||
}
|
||||
await assertXtreamBenchmarkBuildUnchanged(configuration);
|
||||
const summary = createXtreamBenchmarkSummary(
|
||||
manifest,
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
);
|
||||
await writeXtreamSafeJson(
|
||||
join(configuration.outputDirectory, 'summary.json'),
|
||||
summary,
|
||||
safety
|
||||
);
|
||||
if (summary.validForComparison !== (configuration.mode === 'formal')) {
|
||||
throw new Error('xtream-benchmark-comparison-validity-invalid');
|
||||
}
|
||||
console.log(
|
||||
`[performance] completed: ${join(
|
||||
configuration.outputDirectory,
|
||||
'summary.json'
|
||||
)}`
|
||||
);
|
||||
} finally {
|
||||
await control.resetAll().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { MakeDirectoryOptions } from 'node:fs';
|
||||
import { lstat, mkdir, rmdir } from 'node:fs/promises';
|
||||
import {
|
||||
basename,
|
||||
dirname,
|
||||
isAbsolute,
|
||||
join,
|
||||
relative,
|
||||
resolve,
|
||||
sep,
|
||||
} from 'node:path';
|
||||
|
||||
import { assertXtreamSafePathIdentifier } from './xtream-exact-schema';
|
||||
|
||||
export const XTREAM_BENCHMARK_FORMAL_CDP_PORT = 9222;
|
||||
|
||||
const OUTPUT_TIMESTAMP =
|
||||
/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z(?:-[a-z][a-z0-9-]{0,63})?$/u;
|
||||
|
||||
export interface XtreamBenchmarkOutputLayout {
|
||||
readonly outputDirectory: string;
|
||||
readonly outputRoot: string;
|
||||
readonly performanceRoot: string;
|
||||
}
|
||||
|
||||
export type XtreamMkdir = (
|
||||
path: string,
|
||||
options?: MakeDirectoryOptions
|
||||
) => Promise<string | undefined | void>;
|
||||
|
||||
export function resolveXtreamBenchmarkOutputLayout(
|
||||
repositoryRoot: string,
|
||||
requestedRoot: string,
|
||||
variant: string
|
||||
): XtreamBenchmarkOutputLayout {
|
||||
const root = resolve(repositoryRoot);
|
||||
const performanceRoot = join(root, 'dist', 'performance');
|
||||
const outputRoot = resolve(requestedRoot);
|
||||
if (
|
||||
!isAbsolute(repositoryRoot) ||
|
||||
!isAbsolute(requestedRoot) ||
|
||||
requestedRoot !== outputRoot ||
|
||||
dirname(outputRoot) !== performanceRoot ||
|
||||
!isValidTimestampDirectory(basename(outputRoot))
|
||||
) {
|
||||
throw new Error(
|
||||
'Performance output must be an absolute fresh timestamp path below dist/performance'
|
||||
);
|
||||
}
|
||||
try {
|
||||
assertXtreamSafePathIdentifier(variant, 'variant');
|
||||
} catch {
|
||||
throw new Error('IPTVNATOR_PERF_VARIANT must be a safe Xtream variant');
|
||||
}
|
||||
const outputDirectory = join(outputRoot, variant);
|
||||
if (!isStrictDescendant(outputRoot, outputDirectory)) {
|
||||
throw new Error('IPTVNATOR_PERF_VARIANT must be a safe Xtream variant');
|
||||
}
|
||||
return Object.freeze({ outputDirectory, outputRoot, performanceRoot });
|
||||
}
|
||||
|
||||
export function resolveXtreamBenchmarkCdpPort(
|
||||
mode: 'formal' | 'smoke',
|
||||
requestedPort: string | undefined
|
||||
): number {
|
||||
const port =
|
||||
requestedPort === undefined
|
||||
? XTREAM_BENCHMARK_FORMAL_CDP_PORT
|
||||
: Number(requestedPort);
|
||||
if (
|
||||
(requestedPort !== undefined && !/^[1-9]\d*$/u.test(requestedPort)) ||
|
||||
!Number.isSafeInteger(port) ||
|
||||
port < 1024 ||
|
||||
port > 65_535
|
||||
) {
|
||||
throw new Error('IPTVNATOR_PERF_CDP_PORT is invalid');
|
||||
}
|
||||
if (mode === 'formal' && port !== XTREAM_BENCHMARK_FORMAL_CDP_PORT) {
|
||||
throw new Error(
|
||||
`Formal Xtream performance runs require CDP port ${XTREAM_BENCHMARK_FORMAL_CDP_PORT}`
|
||||
);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
export async function assertXtreamBenchmarkOutputPathHasNoSymlinks(
|
||||
repositoryRoot: string,
|
||||
outputRoot: string
|
||||
): Promise<void> {
|
||||
const root = resolve(repositoryRoot);
|
||||
const output = resolve(outputRoot);
|
||||
if (!isStrictDescendant(root, output)) {
|
||||
throw new Error('Xtream performance output escapes the repository');
|
||||
}
|
||||
const components = relative(root, output).split(sep);
|
||||
let current = root;
|
||||
for (const component of ['', ...components]) {
|
||||
current = component ? join(current, component) : current;
|
||||
try {
|
||||
if ((await lstat(current)).isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`Xtream performance output contains a symbolic link: ${current}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isMissingPath(error)) return;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createExclusiveXtreamBenchmarkOutputLayout(
|
||||
repositoryRoot: string,
|
||||
layout: XtreamBenchmarkOutputLayout,
|
||||
createDirectory: XtreamMkdir = mkdir
|
||||
): Promise<void> {
|
||||
await createDirectory(layout.performanceRoot, {
|
||||
mode: 0o700,
|
||||
recursive: true,
|
||||
});
|
||||
await assertXtreamBenchmarkOutputPathHasNoSymlinks(
|
||||
repositoryRoot,
|
||||
layout.outputRoot
|
||||
);
|
||||
await createDirectory(layout.outputRoot, {
|
||||
mode: 0o700,
|
||||
recursive: false,
|
||||
});
|
||||
try {
|
||||
await createDirectory(layout.outputDirectory, {
|
||||
mode: 0o700,
|
||||
recursive: false,
|
||||
});
|
||||
} catch (error) {
|
||||
await rmdir(layout.outputRoot).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
await assertXtreamBenchmarkOutputPathHasNoSymlinks(
|
||||
repositoryRoot,
|
||||
layout.outputDirectory
|
||||
);
|
||||
}
|
||||
|
||||
function isValidTimestampDirectory(value: string): boolean {
|
||||
const match = OUTPUT_TIMESTAMP.exec(value);
|
||||
if (!match) return false;
|
||||
const [, year, month, day, hour, minute, second] = match;
|
||||
const timestamp = Date.UTC(
|
||||
Number(year),
|
||||
Number(month) - 1,
|
||||
Number(day),
|
||||
Number(hour),
|
||||
Number(minute),
|
||||
Number(second)
|
||||
);
|
||||
return (
|
||||
Number.isFinite(timestamp) &&
|
||||
new Date(timestamp).toISOString().replace(/[-:]/gu, '').slice(0, 15) ===
|
||||
`${year}${month}${day}T${hour}${minute}${second}`
|
||||
);
|
||||
}
|
||||
|
||||
function isStrictDescendant(parent: string, candidate: string): boolean {
|
||||
const pathFromParent = relative(resolve(parent), resolve(candidate));
|
||||
return (
|
||||
pathFromParent.length > 0 &&
|
||||
pathFromParent !== '..' &&
|
||||
!pathFromParent.startsWith(`..${sep}`) &&
|
||||
!isAbsolute(pathFromParent)
|
||||
);
|
||||
}
|
||||
|
||||
function isMissingPath(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
error.code === 'ENOENT'
|
||||
);
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
createXtreamIterationDefinitions,
|
||||
XTREAM_ITERATION_KIND,
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamIterationDefinition,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
validateXtreamDiagnosticArtifacts,
|
||||
type XtreamDiagnosticArtifactEvidence,
|
||||
type XtreamDiagnosticCapture,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
|
||||
import {
|
||||
iteration,
|
||||
persist,
|
||||
withWorkerRequestOverlap,
|
||||
} from './xtream-benchmark-report.fixtures';
|
||||
import type { XtreamValidatedIterationResult } from './xtream-summary-contract';
|
||||
|
||||
const HEAP_SNAPSHOT = {
|
||||
edges: [0, 0, 0],
|
||||
nodes: [0, 0, 1, 0, 1, 0],
|
||||
snapshot: {
|
||||
meta: {
|
||||
edge_fields: ['type', 'name_or_index', 'to_node'],
|
||||
edge_types: [['context'], 'string_or_number', 'node'],
|
||||
node_fields: [
|
||||
'type',
|
||||
'name',
|
||||
'id',
|
||||
'self_size',
|
||||
'edge_count',
|
||||
'detachedness',
|
||||
],
|
||||
node_types: [
|
||||
['hidden'],
|
||||
'string',
|
||||
'number',
|
||||
'number',
|
||||
'number',
|
||||
'number',
|
||||
],
|
||||
},
|
||||
},
|
||||
strings: ['', 'root'],
|
||||
};
|
||||
const RENDERER_TRACE = {
|
||||
traceEvents: [{ name: 'xtream-diagnostic', ph: 'X', ts: 1 }],
|
||||
};
|
||||
const directories: string[] = [];
|
||||
|
||||
export interface XtreamReportFixture {
|
||||
readonly diagnosticEvidence: ReadonlyMap<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
>;
|
||||
readonly iterations: readonly XtreamValidatedIterationResult[];
|
||||
}
|
||||
|
||||
export async function formalReportFixture(): Promise<XtreamReportFixture> {
|
||||
return reportFixture(createXtreamIterationDefinitions(false), 10, true);
|
||||
}
|
||||
|
||||
export async function smokeReportFixture(): Promise<XtreamReportFixture> {
|
||||
return reportFixture(createXtreamIterationDefinitions(true), 100, false);
|
||||
}
|
||||
|
||||
export async function clearXtreamProvenanceFixtureDirectories(): Promise<void> {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
}
|
||||
|
||||
async function reportFixture(
|
||||
definitions: readonly XtreamIterationDefinition[],
|
||||
identityOffset: number,
|
||||
mixedInitialOverlap: boolean
|
||||
): Promise<XtreamReportFixture> {
|
||||
const entries = await Promise.all(
|
||||
definitions.map(async (definition, index) => {
|
||||
const identity = index + 1;
|
||||
let raw = iteration(definition, identity);
|
||||
let evidence: XtreamDiagnosticArtifactEvidence | null = null;
|
||||
if (definition.kind === XTREAM_ITERATION_KIND.DIAGNOSTIC) {
|
||||
evidence = await diagnosticEvidence(identity + identityOffset);
|
||||
raw = {
|
||||
...raw,
|
||||
processIdentity: {
|
||||
...raw.processIdentity,
|
||||
profileDirectorySha256: evidence.directorySha256,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (
|
||||
mixedInitialOverlap &&
|
||||
definition.scenarioId ===
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE &&
|
||||
['run-02', 'run-04'].includes(definition.runId)
|
||||
) {
|
||||
raw = withWorkerRequestOverlap(raw, 2);
|
||||
}
|
||||
return {
|
||||
evidence,
|
||||
iteration: await persist(raw, identity + identityOffset),
|
||||
};
|
||||
})
|
||||
);
|
||||
return {
|
||||
diagnosticEvidence: new Map(
|
||||
entries.flatMap(({ evidence, iteration: result }) =>
|
||||
evidence ? [[result, evidence] as const] : []
|
||||
)
|
||||
),
|
||||
iterations: entries.map(({ iteration: result }) => result),
|
||||
};
|
||||
}
|
||||
|
||||
async function diagnosticEvidence(
|
||||
identity: number
|
||||
): Promise<XtreamDiagnosticArtifactEvidence> {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), `iptvnator-xtream-report-diagnostic-${identity}-`)
|
||||
);
|
||||
directories.push(directory);
|
||||
await writeArtifacts(directory);
|
||||
return validateXtreamDiagnosticArtifacts(capture(directory), directory);
|
||||
}
|
||||
|
||||
function capture(directory: string): XtreamDiagnosticCapture {
|
||||
return {
|
||||
main: {
|
||||
cpuProfilePath: join(directory, 'main.cpuprofile'),
|
||||
heapSnapshotPath: join(directory, 'main.heapsnapshot'),
|
||||
workers: [
|
||||
{
|
||||
kind: 'database.worker',
|
||||
ordinal: 1,
|
||||
profilePath: join(
|
||||
directory,
|
||||
'database.worker-1.cpuprofile'
|
||||
),
|
||||
snapshotPath: join(
|
||||
directory,
|
||||
'database.worker-1.heapsnapshot'
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
renderer: {
|
||||
cpuProfilePath: join(directory, 'renderer.cpuprofile'),
|
||||
heapSnapshotPath: join(directory, 'renderer.heapsnapshot'),
|
||||
tracePath: join(directory, 'renderer.trace.json'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function writeArtifacts(directory: string): Promise<void> {
|
||||
const artifacts = [
|
||||
['main.cpuprofile', CPU_PROFILE],
|
||||
['renderer.cpuprofile', CPU_PROFILE],
|
||||
['database.worker-1.cpuprofile', CPU_PROFILE],
|
||||
['main.heapsnapshot', HEAP_SNAPSHOT],
|
||||
['renderer.heapsnapshot', HEAP_SNAPSHOT],
|
||||
['database.worker-1.heapsnapshot', HEAP_SNAPSHOT],
|
||||
['renderer.trace.json', RENDERER_TRACE],
|
||||
] as const;
|
||||
await Promise.all(
|
||||
artifacts.map(([filename, value]) =>
|
||||
writeFile(join(directory, filename), JSON.stringify(value))
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamIterationDefinition,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
computeXtreamBuildFilesAggregate,
|
||||
type XtreamBenchmarkBuildIdentity,
|
||||
type XtreamBuildFileIdentity,
|
||||
} from './xtream-build-identity';
|
||||
import { persistThenValidateXtreamIteration } from './xtream-benchmark-report';
|
||||
import { bindFixtureCancellationEvidence } from './xtream-cancellation-evidence.fixture';
|
||||
import { bindFixtureIpcWorkerIdentities } from './xtream-ipc-worker-identity.fixture';
|
||||
import { scenarioCapture } from './xtream-phase-attribution.fixtures';
|
||||
import { attributeXtreamPhases } from './xtream-phase-attribution';
|
||||
import {
|
||||
XTREAM_MEMORY_ACCOUNTING,
|
||||
type XtreamArtifactSafetyContext,
|
||||
type XtreamBenchmarkManifest,
|
||||
type XtreamPersistenceReceipt,
|
||||
type XtreamRawIterationResult,
|
||||
type XtreamValidatedIterationResult,
|
||||
} from './xtream-summary-contract';
|
||||
import {
|
||||
invalidateWorkerRequestMetrics,
|
||||
workerRequestEvidence,
|
||||
} from './xtream-worker-request-evidence.fixtures';
|
||||
import { bindFixtureWorkerEvidence } from './xtream-worker-phase-evidence.fixture';
|
||||
import type { WorkerRequestPerformanceMetrics } from './m3u-refresh-cancellation-contract';
|
||||
|
||||
export const TOKEN = 'random-control-token-that-must-never-persist';
|
||||
const buildFile = (path: string, marker: string): XtreamBuildFileIdentity => ({
|
||||
bytes: marker.length,
|
||||
path,
|
||||
sha256: sha256(marker),
|
||||
});
|
||||
const rendererFiles = [
|
||||
buildFile('dist/apps/web/index.html', 'renderer-index'),
|
||||
buildFile('dist/apps/web/main-FIXTURE.js', 'renderer-javascript'),
|
||||
buildFile('dist/apps/web/main-FIXTURE.js.map', 'renderer-source-map'),
|
||||
] as const;
|
||||
export const BUILD_IDENTITY: XtreamBenchmarkBuildIdentity = {
|
||||
electron: {
|
||||
databaseWorker: {
|
||||
javascript: buildFile(
|
||||
'dist/apps/electron-backend/workers/database.worker.js',
|
||||
'database-worker-javascript'
|
||||
),
|
||||
sourceMap: buildFile(
|
||||
'dist/apps/electron-backend/workers/database.worker.js.map',
|
||||
'database-worker-source-map'
|
||||
),
|
||||
},
|
||||
main: {
|
||||
javascript: buildFile(
|
||||
'dist/apps/electron-backend/main.js',
|
||||
'main-javascript'
|
||||
),
|
||||
sourceMap: buildFile(
|
||||
'dist/apps/electron-backend/main.js.map',
|
||||
'main-source-map'
|
||||
),
|
||||
},
|
||||
playlistRefreshWorker: {
|
||||
javascript: buildFile(
|
||||
'dist/apps/electron-backend/workers/playlist-refresh.worker.js',
|
||||
'playlist-worker-javascript'
|
||||
),
|
||||
sourceMap: buildFile(
|
||||
'dist/apps/electron-backend/workers/playlist-refresh.worker.js.map',
|
||||
'playlist-worker-source-map'
|
||||
),
|
||||
},
|
||||
preload: {
|
||||
javascript: buildFile(
|
||||
'dist/apps/electron-backend/main.preload.js',
|
||||
'preload-javascript'
|
||||
),
|
||||
sourceMap: buildFile(
|
||||
'dist/apps/electron-backend/main.preload.js.map',
|
||||
'preload-source-map'
|
||||
),
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
aggregateSha256: computeXtreamBuildFilesAggregate(rendererFiles),
|
||||
bytes: rendererFiles.reduce((total, file) => total + file.bytes, 0),
|
||||
executableJavaScriptCount: 1,
|
||||
files: rendererFiles,
|
||||
sourceMapCount: 1,
|
||||
},
|
||||
};
|
||||
const SAFETY: XtreamArtifactSafetyContext = {
|
||||
controlToken: TOKEN,
|
||||
syntheticCredentials: {
|
||||
password: 'performance',
|
||||
username: 'performance',
|
||||
},
|
||||
};
|
||||
export const MANIFEST: XtreamBenchmarkManifest = {
|
||||
build: BUILD_IDENTITY,
|
||||
environment: {
|
||||
cdpAddress: '127.0.0.1',
|
||||
cdpPort: 9222,
|
||||
databaseWorkerBatchDelayMs: 0,
|
||||
exposeGc: true,
|
||||
freshUserDataPerIteration: true,
|
||||
perfCapture: '1',
|
||||
perfWorkerProfiling: '1',
|
||||
traceRendererConsole: '0',
|
||||
},
|
||||
fixture: {
|
||||
bytes: 12_345,
|
||||
catalogSha256: hash('1'),
|
||||
categories: 100,
|
||||
items: 100_000,
|
||||
scenario: 'performance-100k',
|
||||
seed: 91_001,
|
||||
},
|
||||
memoryAccounting: XTREAM_MEMORY_ACCOUNTING,
|
||||
mode: 'formal',
|
||||
runtime: {
|
||||
arch: 'arm64',
|
||||
electronVersion: '38.0.0',
|
||||
harnessNodeVersion: '22.0.0',
|
||||
platform: 'darwin',
|
||||
},
|
||||
source: {
|
||||
gitCommit: '0123456789abcdef0123456789abcdef01234567',
|
||||
gitDiffSha256: hash('2'),
|
||||
gitDirty: false,
|
||||
},
|
||||
suite: 'xtream-performance-100k',
|
||||
variant: 'baseline',
|
||||
};
|
||||
const directories: string[] = [];
|
||||
export async function clearXtreamReportFixtureDirectories(): Promise<void> {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
}
|
||||
|
||||
export async function persist(
|
||||
raw: XtreamRawIterationResult,
|
||||
id: number,
|
||||
afterWrite?: () => void
|
||||
): Promise<XtreamValidatedIterationResult> {
|
||||
return persistWith(raw, async (serialized) => {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), `iptvnator-xtream-raw-${id}-`)
|
||||
);
|
||||
directories.push(directory);
|
||||
const absolutePath = join(directory, 'raw.json');
|
||||
await writeFile(absolutePath, serialized, 'utf8');
|
||||
afterWrite?.();
|
||||
return {
|
||||
absolutePath,
|
||||
bytes: Buffer.byteLength(serialized),
|
||||
sha256: sha256(serialized),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function persistWith(
|
||||
raw: XtreamRawIterationResult,
|
||||
write: (serialized: string) => Promise<XtreamPersistenceReceipt>
|
||||
): Promise<XtreamValidatedIterationResult> {
|
||||
return persistThenValidateXtreamIteration(raw, SAFETY, write);
|
||||
}
|
||||
|
||||
export function iteration(
|
||||
definition: XtreamIterationDefinition,
|
||||
identity: number
|
||||
): XtreamRawIterationResult {
|
||||
const diagnostic = definition.kind === XTREAM_ITERATION_KIND.DIAGNOSTIC;
|
||||
const background =
|
||||
definition.scenarioId === XTREAM_SCENARIO_ID.BACKGROUND_UI;
|
||||
const workerBinding = bindFixtureWorkerEvidence(
|
||||
scenarioCapture(definition.scenarioId),
|
||||
workerRequestEvidence(definition.scenarioId)
|
||||
);
|
||||
const cancellationBinding = bindFixtureCancellationEvidence(
|
||||
workerBinding.phaseCapture,
|
||||
workerBinding.requests
|
||||
);
|
||||
const ipcWorkerBinding = bindFixtureIpcWorkerIdentities(
|
||||
cancellationBinding.phaseCapture,
|
||||
workerBinding.requests
|
||||
);
|
||||
const phaseCapture = ipcWorkerBinding.phaseCapture;
|
||||
const phases = attributeXtreamPhases(phaseCapture);
|
||||
const requestEvidence = ipcWorkerBinding.requests;
|
||||
const capturedRequestEvidence = background
|
||||
? invalidateWorkerRequestMetrics(requestEvidence, 3)
|
||||
: requestEvidence;
|
||||
const requestTotalCount = requestEvidence.length;
|
||||
const profile = {
|
||||
cpuProfileDurationMicros: diagnostic ? 2_000 : null,
|
||||
cpuProfileUnavailableReason: diagnostic
|
||||
? null
|
||||
: 'not-captured-outside-diagnostic',
|
||||
} as const;
|
||||
return {
|
||||
...definition,
|
||||
processIdentity: {
|
||||
captureGeneration: 1,
|
||||
electronPid: 10_000 + identity,
|
||||
freshProcess: true,
|
||||
generationIdentitySha256: numberedHash(identity, 3),
|
||||
launchId: `launch-${identity}`,
|
||||
profileDirectorySha256: numberedHash(identity, 4),
|
||||
},
|
||||
phaseCapture,
|
||||
phases,
|
||||
renderer: {
|
||||
...profile,
|
||||
actionLatencyMs: background ? 7 : null,
|
||||
consoleErrors: [],
|
||||
dashboardPaintVisibility: background
|
||||
? 'visible-without-overlay'
|
||||
: null,
|
||||
frameGapMaxMs: 45,
|
||||
frameGapP95Ms: 20,
|
||||
heartbeatMaxMs: 12,
|
||||
heartbeatP95Ms: 5,
|
||||
longTaskCount: 2,
|
||||
longTaskMaxMs: 75,
|
||||
navigationLatencyMs: background ? 8 : null,
|
||||
peakHeapUsedBytes: 300,
|
||||
peakRssBytes: 500,
|
||||
postGcHeapUsedBytes: 200,
|
||||
searchLatencyMs: background ? 9 : null,
|
||||
storeToPaintMs: phases.angularRenderingMs,
|
||||
targetId: `renderer-${identity}`,
|
||||
},
|
||||
main: {
|
||||
...profile,
|
||||
browserWindowId: 1,
|
||||
cpuMetricScope: 'electron-main-thread',
|
||||
cpuSystemMicros: 200,
|
||||
cpuUserMicros: 1_000,
|
||||
eventLoopDelayMaxMs: 8,
|
||||
eventLoopDelayP95Ms: 2,
|
||||
eventLoopDelayP99Ms: 4,
|
||||
eventLoopUtilization: null,
|
||||
eventLoopUtilizationUnavailableReason:
|
||||
'electron-main-embedded-event-loop',
|
||||
peakHeapUsedBytes: 400,
|
||||
peakRssBytes: 1_000,
|
||||
postGcHeapUsedBytes: 250,
|
||||
responsiveEvents: 0,
|
||||
unresponsiveEvents: 0,
|
||||
webContentsId: 2,
|
||||
},
|
||||
databaseWorker: {
|
||||
...profile,
|
||||
cpuMetricScope: 'sum-valid-request-thread-cpu',
|
||||
cpuSystemMicros:
|
||||
capturedRequestEvidence.filter(
|
||||
({ invalidReason }) => invalidReason === null
|
||||
).length * 10,
|
||||
cpuUserMicros:
|
||||
capturedRequestEvidence.filter(
|
||||
({ invalidReason }) => invalidReason === null
|
||||
).length * 20,
|
||||
currentForCapture: true,
|
||||
eventLoopDelayMaxMs: 9,
|
||||
eventLoopDelayMetricScope: 'valid-request-window-proxy',
|
||||
eventLoopDelayP95Ms: 3,
|
||||
eventLoopDelayP99Ms: 5,
|
||||
eventLoopUtilization: 0.6,
|
||||
kind: 'database.worker',
|
||||
ordinal: 1,
|
||||
peakExternalBytes: 40,
|
||||
peakHeapUsedBytes: 250,
|
||||
postGcHeapUsedBytes: 150,
|
||||
requests: {
|
||||
evidence: capturedRequestEvidence,
|
||||
eventLoopDelayMaxMs: 9,
|
||||
eventLoopDelayP95Ms: 3,
|
||||
eventLoopDelayP99Ms: 5,
|
||||
eventLoopUtilization: 0.5,
|
||||
invalidReasons: background
|
||||
? [
|
||||
{
|
||||
count: 3,
|
||||
reason: 'overlapping-database-worker-requests',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
invalidSampleCount: background ? 3 : 0,
|
||||
metricScope: 'valid-request-window-proxy',
|
||||
totalCount: requestTotalCount,
|
||||
validSampleCount: requestTotalCount - (background ? 3 : 0),
|
||||
},
|
||||
rssBytes: null,
|
||||
rssUnavailableReason: 'unavailable-per-worker-thread',
|
||||
wholeWorkerMetricScope: 'whole-worker-operation-window',
|
||||
},
|
||||
playlistRefreshWorker: {
|
||||
instantiated: false,
|
||||
reason: 'not-instantiated-by-xtream-flow',
|
||||
},
|
||||
cancellation: cancellationBinding.cancellation,
|
||||
cancellationEvidence: cancellationBinding.cancellationEvidence,
|
||||
catalogVerification: {
|
||||
databaseStateValid: true,
|
||||
fixtureIdentityValid: true,
|
||||
providerCategoryCount: 100,
|
||||
providerItemCount: 100_000,
|
||||
verifiedAfterCapture: true,
|
||||
},
|
||||
validity: {
|
||||
actionContractValid: true,
|
||||
catalogContractValid: true,
|
||||
databaseWorkerCount: 1,
|
||||
dbPhaseContractValid: true,
|
||||
diagnosticArtifactsValid: diagnostic ? true : null,
|
||||
lateDatabaseRequestCount: 0,
|
||||
playlistWorkerCount: 0,
|
||||
rendererTargetCount: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function withWorkerRequestOverlap(
|
||||
raw: XtreamRawIterationResult,
|
||||
invalidSampleCount: number
|
||||
): XtreamRawIterationResult {
|
||||
return {
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
requests: {
|
||||
...raw.databaseWorker.requests,
|
||||
evidence: invalidateWorkerRequestMetrics(
|
||||
raw.databaseWorker.requests.evidence.map(
|
||||
restoreValidRequestMetrics
|
||||
),
|
||||
invalidSampleCount
|
||||
),
|
||||
invalidReasons:
|
||||
invalidSampleCount === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
count: invalidSampleCount,
|
||||
reason: 'overlapping-database-worker-requests',
|
||||
},
|
||||
],
|
||||
invalidSampleCount,
|
||||
validSampleCount:
|
||||
raw.databaseWorker.requests.totalCount - invalidSampleCount,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function restoreValidRequestMetrics(
|
||||
request: WorkerRequestPerformanceMetrics
|
||||
): WorkerRequestPerformanceMetrics {
|
||||
if (
|
||||
request.workEndedEpochMs === null ||
|
||||
request.responsePostedEpochMs === null
|
||||
) {
|
||||
throw new Error('invalid Xtream request fixture');
|
||||
}
|
||||
return {
|
||||
...request,
|
||||
eventLoopDelay: { maxMs: 9, p95Ms: 3, p99Ms: 5 },
|
||||
eventLoopDelayUnavailableReason: null,
|
||||
eventLoopUtilization: 0.5,
|
||||
eventLoopUtilizationUnavailableReason: null,
|
||||
histogramFlushedEpochMs:
|
||||
(request.workEndedEpochMs + request.responsePostedEpochMs) / 2,
|
||||
invalidReason: null,
|
||||
threadCpuSystemMicros: 10,
|
||||
threadCpuUnavailableReason: null,
|
||||
threadCpuUserMicros: 20,
|
||||
};
|
||||
}
|
||||
|
||||
export function sha256(value: string): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function hash(character: string): string {
|
||||
return character.repeat(64);
|
||||
}
|
||||
|
||||
function numberedHash(value: number, salt: number): string {
|
||||
return sha256(`${salt}:${value}`);
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
createXtreamIterationDefinitions,
|
||||
type XtreamBenchmarkManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { createXtreamBenchmarkSummary } from './xtream-benchmark-report';
|
||||
import {
|
||||
MANIFEST,
|
||||
TOKEN,
|
||||
clearXtreamReportFixtureDirectories,
|
||||
iteration,
|
||||
persist,
|
||||
persistWith,
|
||||
sha256,
|
||||
} from './xtream-benchmark-report.fixtures';
|
||||
import {
|
||||
clearXtreamProvenanceFixtureDirectories,
|
||||
formalReportFixture,
|
||||
smokeReportFixture,
|
||||
} from './xtream-benchmark-report-provenance.fixtures';
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
clearXtreamReportFixtureDirectories(),
|
||||
clearXtreamProvenanceFixtureDirectories(),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('Xtream benchmark report contract', () => {
|
||||
it('writes exact raw bytes before semantic validation and verifies a real receipt', async () => {
|
||||
const raw = iteration(
|
||||
itemAt(createXtreamIterationDefinitions(false), 1),
|
||||
1
|
||||
);
|
||||
const invalid = {
|
||||
...raw,
|
||||
main: { ...raw.main, peakRssBytes: -1 },
|
||||
};
|
||||
const order: string[] = [];
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
persist(invalid, 1, () => {
|
||||
order.push('raw-written');
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
order.push('validation-rejected');
|
||||
assert.deepEqual(order, ['raw-written', 'validation-rejected']);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
persistWith(raw, async (serialized) => ({
|
||||
absolutePath: join(tmpdir(), 'does-not-exist.json'),
|
||||
bytes: Buffer.byteLength(serialized),
|
||||
sha256: sha256(serialized),
|
||||
})),
|
||||
/persistence receipt/
|
||||
);
|
||||
});
|
||||
|
||||
it('brands the exact persisted bytes when the caller mutates during write', async () => {
|
||||
const raw = iteration(
|
||||
itemAt(createXtreamIterationDefinitions(false), 1),
|
||||
1
|
||||
);
|
||||
const persistedPeakRss = raw.main.peakRssBytes;
|
||||
const validated = await persist(raw, 2, () => {
|
||||
Object.assign(raw.main, { peakRssBytes: persistedPeakRss + 1 });
|
||||
});
|
||||
|
||||
assert.equal(validated.main.peakRssBytes, persistedPeakRss);
|
||||
assert.notEqual(validated.main.peakRssBytes, raw.main.peakRssBytes);
|
||||
assert.throws(
|
||||
() => Object.assign(validated.main, { peakRssBytes: 1 }),
|
||||
TypeError
|
||||
);
|
||||
});
|
||||
|
||||
it('blocks exact token and credential-shaped bytes before write but permits suite labels', async () => {
|
||||
const raw = iteration(
|
||||
itemAt(createXtreamIterationDefinitions(false), 1),
|
||||
1
|
||||
);
|
||||
let writes = 0;
|
||||
for (const leaked of [
|
||||
TOKEN,
|
||||
'performance:performance',
|
||||
'http://performance:performance@127.0.0.1:43123',
|
||||
'?username=performance&password=performance',
|
||||
]) {
|
||||
await assert.rejects(
|
||||
() =>
|
||||
persistWith(
|
||||
{
|
||||
...raw,
|
||||
renderer: {
|
||||
...raw.renderer,
|
||||
consoleErrors: [leaked],
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
writes += 1;
|
||||
throw new Error('must not write');
|
||||
}
|
||||
),
|
||||
/credential-safe/
|
||||
);
|
||||
}
|
||||
assert.equal(writes, 0);
|
||||
await assert.doesNotReject(() => persist(raw, 2));
|
||||
});
|
||||
|
||||
it('requires exact clean formal source, runtime, CDP, and environment evidence', async () => {
|
||||
const { diagnosticEvidence, iterations } = await formalReportFixture();
|
||||
assert.doesNotThrow(() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
)
|
||||
);
|
||||
|
||||
for (const invalid of [
|
||||
{ ...MANIFEST, source: { ...MANIFEST.source, gitDirty: true } },
|
||||
{
|
||||
...MANIFEST,
|
||||
environment: { ...MANIFEST.environment, cdpPort: 9223 },
|
||||
},
|
||||
{
|
||||
...MANIFEST,
|
||||
environment: {
|
||||
...MANIFEST.environment,
|
||||
perfWorkerProfiling: '0',
|
||||
},
|
||||
},
|
||||
{
|
||||
...MANIFEST,
|
||||
runtime: { ...MANIFEST.runtime, electronVersion: '' },
|
||||
},
|
||||
{ ...MANIFEST, variant: { length: 1 } },
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
invalid as unknown as XtreamBenchmarkManifest,
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
),
|
||||
/invalid Xtream benchmark manifest/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('requires ordered definitions and unique process/profile/generation identity', async () => {
|
||||
const { diagnosticEvidence, iterations } = await formalReportFixture();
|
||||
const shuffled = [...iterations];
|
||||
[shuffled[0], shuffled[1]] = [itemAt(shuffled, 1), itemAt(shuffled, 0)];
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
shuffled,
|
||||
diagnosticEvidence
|
||||
),
|
||||
/ordered Xtream benchmark schedule/
|
||||
);
|
||||
|
||||
const reused = [...iterations];
|
||||
const first = itemAt(reused, 0);
|
||||
const second = itemAt(reused, 1);
|
||||
reused[1] = {
|
||||
...second,
|
||||
processIdentity: first.processIdentity,
|
||||
};
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
reused,
|
||||
diagnosticEvidence
|
||||
),
|
||||
/fresh process identity/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects forged validated objects and excludes diagnostic CPU profiles from headline metrics', async () => {
|
||||
const { diagnosticEvidence, iterations } = await formalReportFixture();
|
||||
const forged = [...iterations];
|
||||
forged[0] = { ...itemAt(forged, 0) };
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
forged,
|
||||
diagnosticEvidence
|
||||
),
|
||||
/validated persistence evidence/
|
||||
);
|
||||
|
||||
const summary = createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
);
|
||||
const background = summary.scenarios.find(
|
||||
({ scenarioId }) => scenarioId === 'xtream-background-ui'
|
||||
);
|
||||
const initial = summary.scenarios.find(
|
||||
({ scenarioId }) => scenarioId === 'xtream-initial-import-large'
|
||||
);
|
||||
assert.ok(background);
|
||||
assert.ok(initial);
|
||||
assert.deepEqual(background.databaseWorkerRequestInvalidReasons, [
|
||||
'overlapping-database-worker-requests',
|
||||
]);
|
||||
assert.equal(
|
||||
background.metrics.databaseWorkerRequestInvalidSampleCount.min,
|
||||
3
|
||||
);
|
||||
assert.equal(
|
||||
background.metrics.databaseWorkerRequestInvalidSampleCount.max,
|
||||
3
|
||||
);
|
||||
assert.deepEqual(initial.databaseWorkerRequestInvalidReasons, [
|
||||
'overlapping-database-worker-requests',
|
||||
]);
|
||||
assert.equal(
|
||||
initial.metrics.databaseWorkerRequestInvalidSampleCount.min,
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
initial.metrics.databaseWorkerRequestInvalidSampleCount.max,
|
||||
2
|
||||
);
|
||||
assert.ok(
|
||||
summary.scenarios.every(
|
||||
({
|
||||
databaseWorkerEventLoopDelayMetricScope,
|
||||
databaseWorkerRequestMetricScope,
|
||||
databaseWorkerWholeWorkerMetricScope,
|
||||
metrics,
|
||||
}) =>
|
||||
databaseWorkerEventLoopDelayMetricScope ===
|
||||
'valid-request-window-proxy' &&
|
||||
databaseWorkerRequestMetricScope ===
|
||||
'valid-request-window-proxy' &&
|
||||
databaseWorkerWholeWorkerMetricScope ===
|
||||
'whole-worker-operation-window' &&
|
||||
metrics.databaseWorkerRequestEventLoopDelayP95Ms.count === 5
|
||||
)
|
||||
);
|
||||
assert.equal(summary.validForComparison, true);
|
||||
assert.ok(Object.isFrozen(summary));
|
||||
assert.ok(Object.isFrozen(summary.manifest));
|
||||
assert.ok(Object.isFrozen(summary.iterations));
|
||||
assert.ok(
|
||||
Object.isFrozen(
|
||||
itemAt(summary.iterations, 0).databaseWorker.requests.evidence
|
||||
)
|
||||
);
|
||||
assert.notEqual(summary.manifest, MANIFEST);
|
||||
assert.notEqual(summary.iterations, iterations);
|
||||
assert.ok(
|
||||
summary.scenarios.every(
|
||||
({
|
||||
diagnosticProfiles,
|
||||
mainEventLoopUtilization,
|
||||
measuredRuns,
|
||||
metrics,
|
||||
phases,
|
||||
}) =>
|
||||
measuredRuns === 5 &&
|
||||
metrics.totalMs.count === 5 &&
|
||||
diagnosticProfiles.mainDurationMicros !== null &&
|
||||
diagnosticProfiles.artifactProvenance.artifacts.length ===
|
||||
7 &&
|
||||
Object.isFrozen(
|
||||
diagnosticProfiles.artifactProvenance.artifacts
|
||||
) &&
|
||||
mainEventLoopUtilization.reason ===
|
||||
'electron-main-embedded-event-loop' &&
|
||||
mainEventLoopUtilization.value === null &&
|
||||
phases.indexes.reason ===
|
||||
'schema-indexes-pre-exist; operation-creates-none' &&
|
||||
phases.restoreUserData.value === null &&
|
||||
phases.structuredCloneAttribution ===
|
||||
'selected-ipc-proxy-includes-queueing-and-scheduling' &&
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
metrics,
|
||||
'mainCpuProfileDurationMicros'
|
||||
)
|
||||
)
|
||||
);
|
||||
assert.equal(
|
||||
initial.phases.restoreUserData.reason,
|
||||
'not-invoked-by-scenario'
|
||||
);
|
||||
assert.equal(background.phases.restoreUserData.reason, 'no-user-data');
|
||||
});
|
||||
|
||||
it('requires branded one-to-one diagnostic evidence bound to each iteration directory', async () => {
|
||||
const { diagnosticEvidence, iterations } = await formalReportFixture();
|
||||
const entries = [...diagnosticEvidence.entries()];
|
||||
const first = itemAt(entries, 0);
|
||||
const second = itemAt(entries, 1);
|
||||
const missing = new Map(entries.slice(1));
|
||||
const swapped = new Map(entries);
|
||||
swapped.set(first[0], second[1]);
|
||||
swapped.set(second[0], first[1]);
|
||||
const extra = new Map(entries);
|
||||
extra.set(itemAt(iterations, 0), first[1]);
|
||||
const forged = new Map(entries);
|
||||
forged.set(first[0], structuredClone(first[1]));
|
||||
const malformed = new Map(entries);
|
||||
malformed.set(first[0], null as unknown as (typeof entries)[number][1]);
|
||||
|
||||
for (const evidence of [missing, swapped, extra, forged, malformed]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamBenchmarkSummary(
|
||||
MANIFEST,
|
||||
iterations,
|
||||
evidence
|
||||
),
|
||||
/diagnostic artifact evidence/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps one-run smoke output ineligible after all clean gates pass', async () => {
|
||||
const { diagnosticEvidence, iterations } = await smokeReportFixture();
|
||||
const summary = createXtreamBenchmarkSummary(
|
||||
{ ...MANIFEST, mode: 'smoke' },
|
||||
iterations,
|
||||
diagnosticEvidence
|
||||
);
|
||||
|
||||
assert.equal(summary.validForComparison, false);
|
||||
});
|
||||
});
|
||||
|
||||
function itemAt<T>(items: readonly T[], index: number): T {
|
||||
const item = items[index];
|
||||
assert.ok(item);
|
||||
return item;
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
import {
|
||||
createXtreamIterationDefinitions,
|
||||
listXtreamBenchmarkScenarios,
|
||||
summarizeXtreamNumbers,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
serializeXtreamArtifactForPersistence,
|
||||
type XtreamDiagnosticArtifactEvidence,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import {
|
||||
bindXtreamDiagnosticEvidence,
|
||||
cloneXtreamSummaryValue,
|
||||
copyXtreamDiagnosticProvenance,
|
||||
} from './xtream-benchmark-summary-evidence';
|
||||
import {
|
||||
assertFreshXtreamProcessIdentities,
|
||||
assertOrderedXtreamSchedule,
|
||||
freezeDeep,
|
||||
isFiniteNonNegative,
|
||||
parseXtreamBenchmarkManifest,
|
||||
snapshotXtreamIterations,
|
||||
toXtreamRawIteration,
|
||||
} from './xtream-exact-schema';
|
||||
import { assertXtreamIterationResult } from './xtream-iteration-validity';
|
||||
import { assertUniqueXtreamPersistencePaths } from './xtream-persistence-schedule';
|
||||
import { verifyXtreamPersistenceReceipt } from './xtream-raw-persistence-receipt';
|
||||
import {
|
||||
XTREAM_NOT_APPLICABLE,
|
||||
XTREAM_RENDERER_VISIBILITY,
|
||||
XTREAM_STRUCTURED_CLONE_ATTRIBUTION,
|
||||
XTREAM_WORKER_REQUEST_METRIC_SCOPE,
|
||||
XTREAM_WORKER_WHOLE_METRIC_SCOPE,
|
||||
type NumericDistribution,
|
||||
type XtreamArtifactSafetyContext,
|
||||
type XtreamBenchmarkManifest,
|
||||
type XtreamBenchmarkSummary,
|
||||
type XtreamPersistedRawEvidence,
|
||||
type XtreamPersistenceReceipt,
|
||||
type XtreamRawIterationResult,
|
||||
type XtreamScenarioMetrics,
|
||||
type XtreamScenarioSummary,
|
||||
type XtreamValidatedIterationResult,
|
||||
} from './xtream-summary-contract';
|
||||
|
||||
const VALIDATED = new WeakSet<object>();
|
||||
|
||||
export async function persistThenValidateXtreamIteration(
|
||||
raw: XtreamRawIterationResult,
|
||||
safety: XtreamArtifactSafetyContext,
|
||||
persistRaw: (serialized: string) => Promise<XtreamPersistenceReceipt>
|
||||
): Promise<XtreamValidatedIterationResult> {
|
||||
const serialized = serializeXtreamArtifactForPersistence(raw, safety);
|
||||
const receipt = await persistRaw(serialized);
|
||||
const evidence = await verifyXtreamPersistenceReceipt(serialized, receipt);
|
||||
const persisted: unknown = JSON.parse(serialized);
|
||||
assertXtreamIterationResult(persisted);
|
||||
return brandValidatedIteration(persisted, evidence);
|
||||
}
|
||||
|
||||
export function createXtreamBenchmarkSummary(
|
||||
manifest: XtreamBenchmarkManifest,
|
||||
iterations: readonly XtreamValidatedIterationResult[],
|
||||
diagnosticEvidence: ReadonlyMap<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
>
|
||||
): XtreamBenchmarkSummary {
|
||||
const canonicalManifest = parseXtreamBenchmarkManifest(manifest);
|
||||
const expected = createXtreamIterationDefinitions(
|
||||
canonicalManifest.mode === 'smoke'
|
||||
);
|
||||
const canonicalIterations = snapshotXtreamIterations(iterations);
|
||||
assertOrderedXtreamSchedule(
|
||||
canonicalIterations,
|
||||
expected,
|
||||
canonicalManifest.mode
|
||||
);
|
||||
assertFreshXtreamProcessIdentities(canonicalIterations);
|
||||
for (const iteration of canonicalIterations) {
|
||||
assertValidatedIteration(iteration);
|
||||
if (iteration.validationStatus !== 'valid') invalidIteration();
|
||||
assertXtreamIterationResult(toXtreamRawIteration(iteration));
|
||||
}
|
||||
assertUniqueXtreamPersistencePaths(canonicalIterations);
|
||||
const boundDiagnosticEvidence = bindXtreamDiagnosticEvidence(
|
||||
canonicalIterations,
|
||||
diagnosticEvidence
|
||||
);
|
||||
|
||||
const measuredRunCount = canonicalManifest.mode === 'formal' ? 5 : 1;
|
||||
const comparable =
|
||||
canonicalManifest.mode === 'formal' &&
|
||||
canonicalIterations
|
||||
.filter(({ kind }) => kind === 'measured')
|
||||
.every(({ eligibleForComparison }) => eligibleForComparison);
|
||||
return freezeDeep({
|
||||
iterations: cloneXtreamSummaryValue(canonicalIterations),
|
||||
manifest: cloneXtreamSummaryValue(canonicalManifest),
|
||||
scenarios: listXtreamBenchmarkScenarios().map(({ id }) =>
|
||||
summarizeScenario(
|
||||
canonicalIterations,
|
||||
id,
|
||||
measuredRunCount,
|
||||
boundDiagnosticEvidence
|
||||
)
|
||||
),
|
||||
validForComparison: comparable,
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeScenario(
|
||||
iterations: readonly XtreamValidatedIterationResult[],
|
||||
scenarioId: XtreamScenarioId,
|
||||
measuredRunCount: number,
|
||||
diagnosticEvidence: ReadonlyMap<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
>
|
||||
): XtreamScenarioSummary {
|
||||
const measured = iterations.filter(
|
||||
(iteration) =>
|
||||
iteration.scenarioId === scenarioId && iteration.kind === 'measured'
|
||||
);
|
||||
const diagnostic = iterations.find(
|
||||
(iteration) =>
|
||||
iteration.scenarioId === scenarioId &&
|
||||
iteration.kind === 'diagnostic'
|
||||
);
|
||||
if (measured.length !== measuredRunCount || !diagnostic) {
|
||||
scheduleError(measuredRunCount === 5 ? 'formal' : 'smoke');
|
||||
}
|
||||
const provenance = diagnosticEvidence.get(diagnostic);
|
||||
if (!provenance) invalidIteration();
|
||||
const restores =
|
||||
scenarioId === 'xtream-refresh-large' ||
|
||||
scenarioId === 'xtream-background-ui';
|
||||
return {
|
||||
scenarioId,
|
||||
measuredRuns: measuredRunCount,
|
||||
databaseWorkerEventLoopDelayMetricScope:
|
||||
XTREAM_WORKER_REQUEST_METRIC_SCOPE.VALID_REQUEST_WINDOW_PROXY,
|
||||
databaseWorkerRequestInvalidReasons: [
|
||||
...new Set(
|
||||
measured.flatMap((run) =>
|
||||
run.databaseWorker.requests.invalidReasons.map(
|
||||
({ reason }) => reason
|
||||
)
|
||||
)
|
||||
),
|
||||
],
|
||||
databaseWorkerRequestMetricScope:
|
||||
XTREAM_WORKER_REQUEST_METRIC_SCOPE.VALID_REQUEST_WINDOW_PROXY,
|
||||
databaseWorkerWholeWorkerMetricScope:
|
||||
XTREAM_WORKER_WHOLE_METRIC_SCOPE.OPERATION_WINDOW,
|
||||
dashboardPaintVisibility:
|
||||
scenarioId === 'xtream-background-ui'
|
||||
? XTREAM_RENDERER_VISIBILITY.VISIBLE_WITHOUT_OVERLAY
|
||||
: null,
|
||||
diagnosticProfiles: {
|
||||
artifactProvenance: copyXtreamDiagnosticProvenance(provenance),
|
||||
databaseWorkerDurationMicros:
|
||||
provenance.profiles.databaseWorker.durationMicros,
|
||||
mainDurationMicros: provenance.profiles.main.durationMicros,
|
||||
rendererDurationMicros: provenance.profiles.renderer.durationMicros,
|
||||
},
|
||||
metrics: summarizeMetrics(measured, scenarioId, measuredRunCount),
|
||||
memory: {
|
||||
databaseWorkerRss: notApplicable(
|
||||
XTREAM_NOT_APPLICABLE.DATABASE_WORKER_RSS
|
||||
),
|
||||
nativeAndSqlite: notApplicable(
|
||||
XTREAM_NOT_APPLICABLE.NATIVE_SQLITE_MEMORY
|
||||
),
|
||||
playlistRefreshWorker: notApplicable(
|
||||
XTREAM_NOT_APPLICABLE.PLAYLIST_REFRESH_WORKER
|
||||
),
|
||||
},
|
||||
mainEventLoopUtilization: notApplicable(
|
||||
XTREAM_NOT_APPLICABLE.MAIN_EVENT_LOOP_UTILIZATION
|
||||
),
|
||||
phases: {
|
||||
debugCopy: notApplicable(XTREAM_NOT_APPLICABLE.DEBUG_COPY),
|
||||
indexes: notApplicable(XTREAM_NOT_APPLICABLE.INDEXES_PRE_EXIST),
|
||||
transactionCommit: notApplicable(
|
||||
XTREAM_NOT_APPLICABLE.COMMIT_INCLUDED_IN_SQLITE_WRITE
|
||||
),
|
||||
restoreUserData: notApplicable(
|
||||
restores
|
||||
? XTREAM_NOT_APPLICABLE.RESTORE_NO_USER_DATA
|
||||
: XTREAM_NOT_APPLICABLE.RESTORE_NOT_INVOKED
|
||||
),
|
||||
backgroundSearchMarker:
|
||||
scenarioId === 'xtream-background-ui'
|
||||
? notApplicable(XTREAM_NOT_APPLICABLE.SOURCES_SEARCH_MARKER)
|
||||
: null,
|
||||
structuredCloneAttribution:
|
||||
XTREAM_STRUCTURED_CLONE_ATTRIBUTION.SELECTED_IPC_PROXY,
|
||||
},
|
||||
rendererLongTaskCountTotal: sumValues(
|
||||
measured,
|
||||
(run) => run.renderer.longTaskCount
|
||||
),
|
||||
responsiveEventsTotal: sumValues(
|
||||
measured,
|
||||
(run) => run.main.responsiveEvents
|
||||
),
|
||||
unresponsiveEventsTotal: sumValues(
|
||||
measured,
|
||||
(run) => run.main.unresponsiveEvents
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeMetrics(
|
||||
runs: readonly XtreamValidatedIterationResult[],
|
||||
scenarioId: XtreamScenarioId,
|
||||
count: number
|
||||
): XtreamScenarioMetrics {
|
||||
const metric = (
|
||||
select: (run: XtreamValidatedIterationResult) => number
|
||||
): NumericDistribution => distribution(runs, select, count);
|
||||
const optional = (
|
||||
select: (run: XtreamValidatedIterationResult) => number | null
|
||||
): NumericDistribution =>
|
||||
distribution(runs, (run) => requireNumber(select(run)), count);
|
||||
return {
|
||||
totalMs: metric((run) => run.phases.totalMs),
|
||||
dataAcquisitionMs: metric((run) => run.phases.dataAcquisitionMs),
|
||||
jsonParsingMs: metric((run) => run.phases.jsonParsingMs),
|
||||
normalizationMs: metric((run) => run.phases.normalizationMs),
|
||||
serializationMs: metric((run) => run.phases.serializationMs),
|
||||
deserializationMs: metric((run) => run.phases.deserializationMs),
|
||||
structuredCloneProxyMs: metric(
|
||||
(run) => run.phases.structuredCloneProxyMs
|
||||
),
|
||||
sqliteReadMs: metric((run) => run.phases.sqliteReadMs),
|
||||
sqliteWriteMs: metric((run) => run.phases.sqliteWriteMs),
|
||||
storeUpdateMs: metric((run) => run.phases.storeUpdateMs),
|
||||
angularRenderingMs: metric((run) => run.phases.angularRenderingMs),
|
||||
rendererPeakRssBytes: metric((run) => run.renderer.peakRssBytes),
|
||||
rendererPeakHeapBytes: metric((run) => run.renderer.peakHeapUsedBytes),
|
||||
rendererPostGcHeapBytes: metric(
|
||||
(run) => run.renderer.postGcHeapUsedBytes
|
||||
),
|
||||
rendererLongTaskCount: metric((run) => run.renderer.longTaskCount),
|
||||
rendererLongTaskMaxMs: metric((run) => run.renderer.longTaskMaxMs),
|
||||
rendererFrameGapP95Ms: metric((run) => run.renderer.frameGapP95Ms),
|
||||
rendererFrameGapMaxMs: metric((run) => run.renderer.frameGapMaxMs),
|
||||
rendererHeartbeatP95Ms: metric((run) => run.renderer.heartbeatP95Ms),
|
||||
rendererHeartbeatMaxMs: metric((run) => run.renderer.heartbeatMaxMs),
|
||||
rendererNavigationLatencyMs:
|
||||
scenarioId === 'xtream-background-ui'
|
||||
? optional((run) => run.renderer.navigationLatencyMs)
|
||||
: null,
|
||||
rendererSearchLatencyMs:
|
||||
scenarioId === 'xtream-background-ui'
|
||||
? optional((run) => run.renderer.searchLatencyMs)
|
||||
: null,
|
||||
rendererActionLatencyMs:
|
||||
scenarioId === 'xtream-background-ui'
|
||||
? optional((run) => run.renderer.actionLatencyMs)
|
||||
: null,
|
||||
mainCpuUserMicros: metric((run) => run.main.cpuUserMicros),
|
||||
mainCpuSystemMicros: metric((run) => run.main.cpuSystemMicros),
|
||||
mainPeakRssBytes: metric((run) => run.main.peakRssBytes),
|
||||
mainPeakHeapBytes: metric((run) => run.main.peakHeapUsedBytes),
|
||||
mainPostGcHeapBytes: metric((run) => run.main.postGcHeapUsedBytes),
|
||||
mainEventLoopDelayP95Ms: metric((run) => run.main.eventLoopDelayP95Ms),
|
||||
mainEventLoopDelayP99Ms: metric((run) => run.main.eventLoopDelayP99Ms),
|
||||
mainEventLoopDelayMaxMs: metric((run) => run.main.eventLoopDelayMaxMs),
|
||||
databaseWorkerCpuUserMicros: metric(
|
||||
(run) => run.databaseWorker.cpuUserMicros
|
||||
),
|
||||
databaseWorkerCpuSystemMicros: metric(
|
||||
(run) => run.databaseWorker.cpuSystemMicros
|
||||
),
|
||||
databaseWorkerPeakHeapBytes: metric(
|
||||
(run) => run.databaseWorker.peakHeapUsedBytes
|
||||
),
|
||||
databaseWorkerPostGcHeapBytes: metric(
|
||||
(run) => run.databaseWorker.postGcHeapUsedBytes
|
||||
),
|
||||
databaseWorkerRequestTotalCount: metric(
|
||||
(run) => run.databaseWorker.requests.totalCount
|
||||
),
|
||||
databaseWorkerRequestValidSampleCount: metric(
|
||||
(run) => run.databaseWorker.requests.validSampleCount
|
||||
),
|
||||
databaseWorkerRequestInvalidSampleCount: metric(
|
||||
(run) => run.databaseWorker.requests.invalidSampleCount
|
||||
),
|
||||
databaseWorkerRequestEventLoopDelayP95Ms: metric(
|
||||
(run) => run.databaseWorker.requests.eventLoopDelayP95Ms
|
||||
),
|
||||
databaseWorkerRequestEventLoopDelayP99Ms: metric(
|
||||
(run) => run.databaseWorker.requests.eventLoopDelayP99Ms
|
||||
),
|
||||
databaseWorkerRequestEventLoopDelayMaxMs: metric(
|
||||
(run) => run.databaseWorker.requests.eventLoopDelayMaxMs
|
||||
),
|
||||
databaseWorkerRequestEventLoopUtilization: metric(
|
||||
(run) => run.databaseWorker.requests.eventLoopUtilization
|
||||
),
|
||||
databaseWorkerPeakExternalBytes: metric(
|
||||
(run) => run.databaseWorker.peakExternalBytes
|
||||
),
|
||||
databaseWorkerEventLoopDelayP95Ms: metric(
|
||||
(run) => run.databaseWorker.eventLoopDelayP95Ms
|
||||
),
|
||||
databaseWorkerEventLoopDelayP99Ms: metric(
|
||||
(run) => run.databaseWorker.eventLoopDelayP99Ms
|
||||
),
|
||||
databaseWorkerEventLoopDelayMaxMs: metric(
|
||||
(run) => run.databaseWorker.eventLoopDelayMaxMs
|
||||
),
|
||||
databaseWorkerEventLoopUtilization: metric(
|
||||
(run) => run.databaseWorker.eventLoopUtilization
|
||||
),
|
||||
cancelAcknowledgementLatencyMs:
|
||||
scenarioId === 'xtream-cancel-import'
|
||||
? optional((run) => run.cancellation.acknowledgementLatencyMs)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function distribution(
|
||||
runs: readonly XtreamValidatedIterationResult[],
|
||||
select: (run: XtreamValidatedIterationResult) => number,
|
||||
expectedCount: number
|
||||
): NumericDistribution {
|
||||
const values = runs.map(select);
|
||||
if (
|
||||
values.length !== expectedCount ||
|
||||
values.some((value) => !isFiniteNonNegative(value))
|
||||
) {
|
||||
throw new Error(
|
||||
'Xtream metric requires complete finite measured values'
|
||||
);
|
||||
}
|
||||
return summarizeXtreamNumbers(values);
|
||||
}
|
||||
|
||||
function sumValues(
|
||||
runs: readonly XtreamValidatedIterationResult[],
|
||||
select: (run: XtreamValidatedIterationResult) => number
|
||||
): number {
|
||||
return runs.reduce((total, run) => total + select(run), 0);
|
||||
}
|
||||
|
||||
function requireNumber(value: number | null): number {
|
||||
if (!isFiniteNonNegative(value)) invalidIteration();
|
||||
return value;
|
||||
}
|
||||
|
||||
function notApplicable(reason: string): { reason: string; value: null } {
|
||||
return { reason, value: null };
|
||||
}
|
||||
|
||||
function brandValidatedIteration(
|
||||
raw: XtreamRawIterationResult,
|
||||
persistence: XtreamPersistedRawEvidence
|
||||
): XtreamValidatedIterationResult {
|
||||
const result = freezeDeep({
|
||||
...raw,
|
||||
persistence,
|
||||
validationStatus: 'valid' as const,
|
||||
});
|
||||
VALIDATED.add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function assertValidatedIteration(value: XtreamValidatedIterationResult): void {
|
||||
if (!VALIDATED.has(value)) {
|
||||
throw new Error('Xtream validated persistence evidence is missing');
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleError(mode: 'formal' | 'smoke'): never {
|
||||
throw new Error(
|
||||
`Xtream benchmark requires exactly ${mode === 'formal' ? 'five' : 'one'} measured runs per scenario`
|
||||
);
|
||||
}
|
||||
|
||||
function invalidIteration(): never {
|
||||
throw new Error('invalid Xtream iteration');
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
deriveXtreamMeasuredPlaylistId,
|
||||
xtreamScenarioRequiresSeed,
|
||||
} from './xtream-benchmark-runtime';
|
||||
|
||||
describe('Xtream benchmark runtime evidence', () => {
|
||||
it('seeds only scenarios that measure an operation on an existing portal', () => {
|
||||
const expected: Record<XtreamScenarioId, boolean> = {
|
||||
[XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE]: false,
|
||||
[XTREAM_SCENARIO_ID.REFRESH_LARGE]: true,
|
||||
[XTREAM_SCENARIO_ID.DELETE_LARGE]: true,
|
||||
[XTREAM_SCENARIO_ID.CANCEL_IMPORT]: false,
|
||||
[XTREAM_SCENARIO_ID.BACKGROUND_UI]: true,
|
||||
};
|
||||
|
||||
for (const scenarioId of Object.values(XTREAM_SCENARIO_ID)) {
|
||||
assert.equal(
|
||||
xtreamScenarioRequiresSeed(scenarioId),
|
||||
expected[scenarioId]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('derives exactly one non-null measured playlist identity', () => {
|
||||
assert.equal(
|
||||
deriveXtreamMeasuredPlaylistId([
|
||||
{ playlistId: null },
|
||||
{ playlistId: 'playlist-1' },
|
||||
{ playlistId: 'playlist-1' },
|
||||
]),
|
||||
'playlist-1'
|
||||
);
|
||||
|
||||
for (const requests of [
|
||||
[{ playlistId: null }],
|
||||
[{ playlistId: 'playlist-1' }, { playlistId: 'playlist-2' }],
|
||||
[{ playlistId: '' }],
|
||||
]) {
|
||||
assert.throws(
|
||||
() => deriveXtreamMeasuredPlaylistId(requests),
|
||||
/xtream-measured-playlist-identity-invalid/
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
|
||||
export function xtreamScenarioRequiresSeed(
|
||||
scenarioId: XtreamScenarioId
|
||||
): boolean {
|
||||
return (
|
||||
scenarioId === XTREAM_SCENARIO_ID.REFRESH_LARGE ||
|
||||
scenarioId === XTREAM_SCENARIO_ID.DELETE_LARGE ||
|
||||
scenarioId === XTREAM_SCENARIO_ID.BACKGROUND_UI
|
||||
);
|
||||
}
|
||||
|
||||
export function deriveXtreamMeasuredPlaylistId(
|
||||
requests: readonly { readonly playlistId: string | null }[]
|
||||
): string {
|
||||
const identities = new Set(
|
||||
requests
|
||||
.map(({ playlistId }) => playlistId)
|
||||
.filter(
|
||||
(playlistId): playlistId is string =>
|
||||
typeof playlistId === 'string' && playlistId.length > 0
|
||||
)
|
||||
);
|
||||
const playlistId = identities.values().next().value as string | undefined;
|
||||
if (identities.size !== 1 || !playlistId) {
|
||||
throw new Error('xtream-measured-playlist-identity-invalid');
|
||||
}
|
||||
return playlistId;
|
||||
}
|
||||
|
||||
export async function disableGenericElectronFixtureProbes(
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
window.__portalDebugUnsubscribe?.();
|
||||
window.__portalDebugUnsubscribe = undefined;
|
||||
window.__portalDebugEvents = [];
|
||||
window.__dbOperationUnsubscribe?.();
|
||||
window.__dbOperationUnsubscribe = undefined;
|
||||
window.__dbOperationEvents = [];
|
||||
if (window.__rendererFrameRequestId) {
|
||||
window.cancelAnimationFrame(window.__rendererFrameRequestId);
|
||||
}
|
||||
window.__rendererFrameRequestId = undefined;
|
||||
window.__rendererFrameCount = 0;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
assertXtreamDiagnosticArtifactEvidence,
|
||||
type XtreamDiagnosticArtifactEvidence,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import type {
|
||||
XtreamDiagnosticArtifactProvenance,
|
||||
XtreamValidatedIterationResult,
|
||||
} from './xtream-summary-contract';
|
||||
|
||||
export function bindXtreamDiagnosticEvidence(
|
||||
iterations: readonly XtreamValidatedIterationResult[],
|
||||
evidenceByIteration: ReadonlyMap<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
>
|
||||
): ReadonlyMap<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
> {
|
||||
const diagnostic = iterations.filter(({ kind }) => kind === 'diagnostic');
|
||||
const evidenceEntries = snapshotEvidenceEntries(evidenceByIteration);
|
||||
if (evidenceEntries.length !== diagnostic.length) {
|
||||
invalidEvidence();
|
||||
}
|
||||
const diagnosticSet = new Set(diagnostic);
|
||||
const boundEvidence = new Map<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
>();
|
||||
for (const [iteration, evidence] of evidenceEntries) {
|
||||
if (!diagnosticSet.has(iteration)) {
|
||||
invalidEvidence();
|
||||
}
|
||||
try {
|
||||
assertXtreamDiagnosticArtifactEvidence(evidence);
|
||||
} catch {
|
||||
invalidEvidence();
|
||||
}
|
||||
if (
|
||||
iteration.processIdentity.profileDirectorySha256 !==
|
||||
evidence.directorySha256 ||
|
||||
iteration.renderer.cpuProfileDurationMicros !==
|
||||
evidence.profiles.renderer.durationMicros ||
|
||||
iteration.main.cpuProfileDurationMicros !==
|
||||
evidence.profiles.main.durationMicros ||
|
||||
iteration.databaseWorker.cpuProfileDurationMicros !==
|
||||
evidence.profiles.databaseWorker.durationMicros ||
|
||||
iteration.databaseWorker.ordinal !==
|
||||
evidence.profiles.databaseWorker.ordinal
|
||||
) {
|
||||
invalidEvidence();
|
||||
}
|
||||
Map.prototype.set.call(boundEvidence, iteration, evidence);
|
||||
}
|
||||
if (
|
||||
diagnostic.some(
|
||||
(iteration) => !Map.prototype.has.call(boundEvidence, iteration)
|
||||
)
|
||||
) {
|
||||
invalidEvidence();
|
||||
}
|
||||
return boundEvidence;
|
||||
}
|
||||
|
||||
export function copyXtreamDiagnosticProvenance(
|
||||
evidence: XtreamDiagnosticArtifactEvidence
|
||||
): XtreamDiagnosticArtifactProvenance {
|
||||
assertXtreamDiagnosticArtifactEvidence(evidence);
|
||||
return {
|
||||
artifacts: evidence.artifacts.map((artifact) => ({ ...artifact })),
|
||||
directorySha256: evidence.directorySha256,
|
||||
};
|
||||
}
|
||||
|
||||
export function cloneXtreamSummaryValue<T>(value: T): T {
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
function snapshotEvidenceEntries(
|
||||
value: ReadonlyMap<
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence
|
||||
>
|
||||
): readonly (readonly [
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence,
|
||||
])[] {
|
||||
try {
|
||||
if (!(value instanceof Map)) invalidEvidence();
|
||||
const sizeGetter = Object.getOwnPropertyDescriptor(
|
||||
Map.prototype,
|
||||
'size'
|
||||
)?.get;
|
||||
if (!sizeGetter) invalidEvidence();
|
||||
const size = Reflect.apply(sizeGetter, value, []) as unknown;
|
||||
const entries: [
|
||||
XtreamValidatedIterationResult,
|
||||
XtreamDiagnosticArtifactEvidence,
|
||||
][] = [];
|
||||
Map.prototype.forEach.call(value, (evidence, iteration) => {
|
||||
entries.push([iteration, evidence]);
|
||||
});
|
||||
if (size !== entries.length) invalidEvidence();
|
||||
return entries;
|
||||
} catch {
|
||||
invalidEvidence();
|
||||
}
|
||||
}
|
||||
|
||||
function invalidEvidence(): never {
|
||||
throw new Error('invalid Xtream diagnostic artifact evidence binding');
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
computeXtreamBuildFilesAggregate,
|
||||
type XtreamBenchmarkBuildIdentity,
|
||||
type XtreamBuildFileIdentity,
|
||||
type XtreamBuildPairIdentity,
|
||||
} from './xtream-build-identity';
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/u;
|
||||
const RENDERER_FILE =
|
||||
/^dist\/apps\/web\/[A-Za-z0-9][A-Za-z0-9._-]{0,254}\.js(?:\.map)?$/u;
|
||||
const BUILD_KEYS = ['electron', 'renderer'] as const;
|
||||
const ELECTRON_KEYS = [
|
||||
'databaseWorker',
|
||||
'main',
|
||||
'playlistRefreshWorker',
|
||||
'preload',
|
||||
] as const;
|
||||
const PAIR_KEYS = ['javascript', 'sourceMap'] as const;
|
||||
const FILE_KEYS = ['bytes', 'path', 'sha256'] as const;
|
||||
const RENDERER_KEYS = [
|
||||
'aggregateSha256',
|
||||
'bytes',
|
||||
'executableJavaScriptCount',
|
||||
'files',
|
||||
'sourceMapCount',
|
||||
] as const;
|
||||
const ELECTRON_PATHS = {
|
||||
databaseWorker: 'dist/apps/electron-backend/workers/database.worker.js',
|
||||
main: 'dist/apps/electron-backend/main.js',
|
||||
playlistRefreshWorker:
|
||||
'dist/apps/electron-backend/workers/playlist-refresh.worker.js',
|
||||
preload: 'dist/apps/electron-backend/main.preload.js',
|
||||
} as const;
|
||||
|
||||
export function parseXtreamBenchmarkBuildIdentity(
|
||||
value: unknown
|
||||
): XtreamBenchmarkBuildIdentity {
|
||||
try {
|
||||
const input = exactRecord(value, BUILD_KEYS);
|
||||
const electronInput = exactRecord(input['electron'], ELECTRON_KEYS);
|
||||
const rendererInput = exactRecord(input['renderer'], RENDERER_KEYS);
|
||||
const electron = Object.freeze({
|
||||
databaseWorker: pair(
|
||||
electronInput['databaseWorker'],
|
||||
ELECTRON_PATHS.databaseWorker
|
||||
),
|
||||
main: pair(electronInput['main'], ELECTRON_PATHS.main),
|
||||
playlistRefreshWorker: pair(
|
||||
electronInput['playlistRefreshWorker'],
|
||||
ELECTRON_PATHS.playlistRefreshWorker
|
||||
),
|
||||
preload: pair(electronInput['preload'], ELECTRON_PATHS.preload),
|
||||
});
|
||||
const renderer = rendererIdentity(rendererInput);
|
||||
return Object.freeze({ electron, renderer });
|
||||
} catch {
|
||||
throw new Error('invalid Xtream benchmark build identity');
|
||||
}
|
||||
}
|
||||
|
||||
function pair(value: unknown, javascriptPath: string): XtreamBuildPairIdentity {
|
||||
const input = exactRecord(value, PAIR_KEYS);
|
||||
return Object.freeze({
|
||||
javascript: fileIdentity(input['javascript'], javascriptPath),
|
||||
sourceMap: fileIdentity(input['sourceMap'], `${javascriptPath}.map`),
|
||||
});
|
||||
}
|
||||
|
||||
function rendererIdentity(
|
||||
input: Record<string, unknown>
|
||||
): XtreamBenchmarkBuildIdentity['renderer'] {
|
||||
const files = exactArray(input['files']).map((value) =>
|
||||
fileIdentity(value)
|
||||
);
|
||||
if (
|
||||
files.length < 3 ||
|
||||
files.length > 10_000 ||
|
||||
!isStrictlySorted(files.map((file) => file.path))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const indexFiles = files.filter(
|
||||
(file) => file.path === 'dist/apps/web/index.html'
|
||||
);
|
||||
const javascript = files.filter((file) => file.path.endsWith('.js'));
|
||||
const sourceMaps = files.filter((file) => file.path.endsWith('.js.map'));
|
||||
if (
|
||||
indexFiles.length !== 1 ||
|
||||
javascript.length === 0 ||
|
||||
files.length !== 1 + javascript.length + sourceMaps.length ||
|
||||
javascript.length !== sourceMaps.length ||
|
||||
javascript.some(
|
||||
(file) =>
|
||||
!sourceMaps.some(
|
||||
(sourceMap) => sourceMap.path === `${file.path}.map`
|
||||
)
|
||||
) ||
|
||||
sourceMaps.some(
|
||||
(sourceMap) =>
|
||||
!javascript.some(
|
||||
(file) => file.path === sourceMap.path.slice(0, -4)
|
||||
)
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const bytes = files.reduce((total, file) => total + file.bytes, 0);
|
||||
if (
|
||||
!isPositiveSafeInteger(input['bytes']) ||
|
||||
input['bytes'] !== bytes ||
|
||||
input['executableJavaScriptCount'] !== javascript.length ||
|
||||
input['sourceMapCount'] !== sourceMaps.length ||
|
||||
typeof input['aggregateSha256'] !== 'string' ||
|
||||
!SHA256.test(input['aggregateSha256']) ||
|
||||
input['aggregateSha256'] !== computeXtreamBuildFilesAggregate(files)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.freeze({
|
||||
aggregateSha256: input['aggregateSha256'],
|
||||
bytes,
|
||||
executableJavaScriptCount: javascript.length,
|
||||
files: Object.freeze(files),
|
||||
sourceMapCount: sourceMaps.length,
|
||||
}) as XtreamBenchmarkBuildIdentity['renderer'];
|
||||
}
|
||||
|
||||
function fileIdentity(
|
||||
value: unknown,
|
||||
expectedPath?: string
|
||||
): XtreamBuildFileIdentity {
|
||||
const input = exactRecord(value, FILE_KEYS);
|
||||
const path = input['path'];
|
||||
const validRendererPath =
|
||||
path === 'dist/apps/web/index.html' ||
|
||||
(typeof path === 'string' && RENDERER_FILE.test(path));
|
||||
if (
|
||||
!isPositiveSafeInteger(input['bytes']) ||
|
||||
typeof path !== 'string' ||
|
||||
(expectedPath === undefined
|
||||
? !validRendererPath
|
||||
: path !== expectedPath) ||
|
||||
typeof input['sha256'] !== 'string' ||
|
||||
!SHA256.test(input['sha256'])
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.freeze({
|
||||
bytes: input['bytes'],
|
||||
path,
|
||||
sha256: input['sha256'],
|
||||
}) as XtreamBuildFileIdentity;
|
||||
}
|
||||
|
||||
function exactArray(value: unknown): unknown[] {
|
||||
if (!Array.isArray(value)) invalid();
|
||||
const length = Reflect.get(value, 'length') as unknown;
|
||||
if (
|
||||
!Number.isSafeInteger(length) ||
|
||||
Number(length) < 0 ||
|
||||
Number(length) > 10_000
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const expectedKeys = new Set([
|
||||
'length',
|
||||
...Array.from({ length: Number(length) }, (_, index) => String(index)),
|
||||
]);
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expectedKeys.size ||
|
||||
keys.some((key) => typeof key !== 'string' || !expectedKeys.has(key))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Array.from({ length: Number(length) }, (_, index) =>
|
||||
Reflect.get(value, String(index))
|
||||
);
|
||||
}
|
||||
|
||||
function exactRecord(
|
||||
value: unknown,
|
||||
expectedKeys: readonly string[]
|
||||
): Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
invalid();
|
||||
}
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expectedKeys.length ||
|
||||
keys.some(
|
||||
(key) => typeof key !== 'string' || !expectedKeys.includes(key)
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.fromEntries(
|
||||
expectedKeys.map((key) => [key, Reflect.get(value, key)])
|
||||
);
|
||||
}
|
||||
|
||||
function isStrictlySorted(values: readonly string[]): boolean {
|
||||
return values.every(
|
||||
(value, index) => index === 0 || (values[index - 1] ?? '') < value
|
||||
);
|
||||
}
|
||||
|
||||
function isPositiveSafeInteger(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
link,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
rm,
|
||||
symlink,
|
||||
unlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
captureXtreamBuildIdentity,
|
||||
computeXtreamBuildFilesAggregate,
|
||||
} from './xtream-build-identity';
|
||||
import { assertXtreamBenchmarkManifest } from './xtream-exact-schema';
|
||||
import { BUILD_IDENTITY, MANIFEST } from './xtream-benchmark-report.fixtures';
|
||||
|
||||
const directories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
});
|
||||
|
||||
describe('Xtream benchmark build identity', () => {
|
||||
it('hashes exact Electron pairs and sorted top-level renderer code', async () => {
|
||||
const root = await buildFixture();
|
||||
|
||||
const identity = await captureXtreamBuildIdentity(root);
|
||||
|
||||
assert.deepEqual(identity.electron.main.javascript, {
|
||||
bytes: 4,
|
||||
path: 'dist/apps/electron-backend/main.js',
|
||||
sha256: sha256('main'),
|
||||
});
|
||||
assert.deepEqual(identity.electron.preload.sourceMap, {
|
||||
bytes: 11,
|
||||
path: 'dist/apps/electron-backend/main.preload.js.map',
|
||||
sha256: sha256('preload-map'),
|
||||
});
|
||||
assert.deepEqual(
|
||||
identity.renderer.files.map((file) => file.path),
|
||||
[
|
||||
'dist/apps/web/chunk-A.js',
|
||||
'dist/apps/web/chunk-A.js.map',
|
||||
'dist/apps/web/index.html',
|
||||
'dist/apps/web/main-A.js',
|
||||
'dist/apps/web/main-A.js.map',
|
||||
]
|
||||
);
|
||||
assert.equal(identity.renderer.executableJavaScriptCount, 2);
|
||||
assert.equal(identity.renderer.sourceMapCount, 2);
|
||||
assert.equal(
|
||||
identity.renderer.bytes,
|
||||
identity.renderer.files.reduce(
|
||||
(total, file) => total + file.bytes,
|
||||
0
|
||||
)
|
||||
);
|
||||
assert.equal(
|
||||
identity.renderer.aggregateSha256,
|
||||
computeXtreamBuildFilesAggregate(identity.renderer.files)
|
||||
);
|
||||
assert.equal(
|
||||
identity.renderer.files.some((file) =>
|
||||
file.path.includes('app-config.js')
|
||||
),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('fails closed when an executable or its source map is missing', async () => {
|
||||
const missingElectron = await buildFixture();
|
||||
await unlink(
|
||||
join(
|
||||
missingElectron,
|
||||
'dist/apps/electron-backend/workers/database.worker.js.map'
|
||||
)
|
||||
);
|
||||
await assert.rejects(
|
||||
captureXtreamBuildIdentity(missingElectron),
|
||||
/invalid Xtream performance build/
|
||||
);
|
||||
|
||||
const missingRendererMap = await buildFixture();
|
||||
await unlink(join(missingRendererMap, 'dist/apps/web/chunk-A.js.map'));
|
||||
await assert.rejects(
|
||||
captureXtreamBuildIdentity(missingRendererMap),
|
||||
/invalid Xtream performance build/
|
||||
);
|
||||
|
||||
const orphanRendererMap = await buildFixture();
|
||||
await writeFile(
|
||||
join(orphanRendererMap, 'dist/apps/web/orphan.js.map'),
|
||||
'orphan'
|
||||
);
|
||||
await assert.rejects(
|
||||
captureXtreamBuildIdentity(orphanRendererMap),
|
||||
/invalid Xtream performance build/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects symlinked and hard-linked build evidence', async () => {
|
||||
const symlinkRoot = await buildFixture();
|
||||
const mainPath = join(
|
||||
symlinkRoot,
|
||||
'dist/apps/electron-backend/main.js'
|
||||
);
|
||||
const external = join(symlinkRoot, 'external-main.js');
|
||||
await writeFile(external, 'main');
|
||||
await unlink(mainPath);
|
||||
await symlink(external, mainPath);
|
||||
await assert.rejects(
|
||||
captureXtreamBuildIdentity(symlinkRoot),
|
||||
/invalid Xtream performance build/
|
||||
);
|
||||
|
||||
const hardLinkRoot = await buildFixture();
|
||||
await link(
|
||||
join(hardLinkRoot, 'dist/apps/web/main-A.js'),
|
||||
join(hardLinkRoot, 'main-alias.js')
|
||||
);
|
||||
await assert.rejects(
|
||||
captureXtreamBuildIdentity(hardLinkRoot),
|
||||
/invalid Xtream performance build/
|
||||
);
|
||||
});
|
||||
|
||||
it('produces a different aggregate for path, byte, or hash drift', () => {
|
||||
const files = [
|
||||
{ bytes: 1, path: 'a.js', sha256: 'a'.repeat(64) },
|
||||
{ bytes: 2, path: 'a.js.map', sha256: 'b'.repeat(64) },
|
||||
];
|
||||
const baseline = computeXtreamBuildFilesAggregate(files);
|
||||
|
||||
for (const changed of [
|
||||
[{ ...files[0], path: 'b.js' }, files[1]],
|
||||
[{ ...files[0], bytes: 2 }, files[1]],
|
||||
[{ ...files[0], sha256: 'c'.repeat(64) }, files[1]],
|
||||
]) {
|
||||
assert.notEqual(
|
||||
computeXtreamBuildFilesAggregate(changed),
|
||||
baseline
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('binds exact build descriptors into the persisted benchmark manifest', () => {
|
||||
assert.doesNotThrow(() => assertXtreamBenchmarkManifest(MANIFEST));
|
||||
const files = BUILD_IDENTITY.renderer.files;
|
||||
const invalidBuilds = [
|
||||
{
|
||||
...BUILD_IDENTITY,
|
||||
renderer: {
|
||||
...BUILD_IDENTITY.renderer,
|
||||
aggregateSha256: 'f'.repeat(64),
|
||||
},
|
||||
},
|
||||
{
|
||||
...BUILD_IDENTITY,
|
||||
renderer: {
|
||||
...BUILD_IDENTITY.renderer,
|
||||
bytes: BUILD_IDENTITY.renderer.bytes + 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
...BUILD_IDENTITY,
|
||||
renderer: {
|
||||
...BUILD_IDENTITY.renderer,
|
||||
files: [...files].reverse(),
|
||||
},
|
||||
},
|
||||
{
|
||||
...BUILD_IDENTITY,
|
||||
renderer: {
|
||||
...BUILD_IDENTITY.renderer,
|
||||
files: files.slice(0, -1),
|
||||
},
|
||||
},
|
||||
{
|
||||
...BUILD_IDENTITY,
|
||||
electron: {
|
||||
...BUILD_IDENTITY.electron,
|
||||
main: {
|
||||
...BUILD_IDENTITY.electron.main,
|
||||
javascript: {
|
||||
...BUILD_IDENTITY.electron.main.javascript,
|
||||
path: 'dist/apps/electron-backend/other.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const build of invalidBuilds) {
|
||||
assert.throws(
|
||||
() => assertXtreamBenchmarkManifest({ ...MANIFEST, build }),
|
||||
/invalid Xtream benchmark manifest/
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function buildFixture(): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'iptvnator-xtream-build-'));
|
||||
directories.push(root);
|
||||
const backend = join(root, 'dist/apps/electron-backend');
|
||||
const workers = join(backend, 'workers');
|
||||
const renderer = join(root, 'dist/apps/web');
|
||||
await Promise.all([
|
||||
mkdir(workers, { recursive: true }),
|
||||
mkdir(join(renderer, 'assets'), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(backend, 'main.js'), 'main'),
|
||||
writeFile(join(backend, 'main.js.map'), 'main-map'),
|
||||
writeFile(join(backend, 'main.preload.js'), 'preload'),
|
||||
writeFile(join(backend, 'main.preload.js.map'), 'preload-map'),
|
||||
writeFile(join(workers, 'database.worker.js'), 'database'),
|
||||
writeFile(join(workers, 'database.worker.js.map'), 'database-map'),
|
||||
writeFile(join(workers, 'playlist-refresh.worker.js'), 'refresh'),
|
||||
writeFile(
|
||||
join(workers, 'playlist-refresh.worker.js.map'),
|
||||
'refresh-map'
|
||||
),
|
||||
writeFile(join(renderer, 'index.html'), '<app-root/>'),
|
||||
writeFile(join(renderer, 'main-A.js'), 'renderer-main'),
|
||||
writeFile(join(renderer, 'main-A.js.map'), 'renderer-main-map'),
|
||||
writeFile(join(renderer, 'chunk-A.js'), 'renderer-chunk'),
|
||||
writeFile(join(renderer, 'chunk-A.js.map'), 'renderer-chunk-map'),
|
||||
writeFile(join(renderer, 'styles.css'), 'ignored'),
|
||||
writeFile(join(renderer, 'assets/app-config.js'), 'ignored'),
|
||||
]);
|
||||
return root;
|
||||
}
|
||||
|
||||
function sha256(value: string): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { isAbsolute, join } from 'node:path';
|
||||
|
||||
import { readXtreamSafeRegularFile } from './xtream-safe-file-read';
|
||||
|
||||
export interface XtreamBuildFileIdentity {
|
||||
readonly bytes: number;
|
||||
readonly path: string;
|
||||
readonly sha256: string;
|
||||
}
|
||||
|
||||
export interface XtreamBuildPairIdentity {
|
||||
readonly javascript: XtreamBuildFileIdentity;
|
||||
readonly sourceMap: XtreamBuildFileIdentity;
|
||||
}
|
||||
|
||||
export interface XtreamBenchmarkBuildIdentity {
|
||||
readonly electron: {
|
||||
readonly databaseWorker: XtreamBuildPairIdentity;
|
||||
readonly main: XtreamBuildPairIdentity;
|
||||
readonly playlistRefreshWorker: XtreamBuildPairIdentity;
|
||||
readonly preload: XtreamBuildPairIdentity;
|
||||
};
|
||||
readonly renderer: {
|
||||
readonly aggregateSha256: string;
|
||||
readonly bytes: number;
|
||||
readonly executableJavaScriptCount: number;
|
||||
readonly files: readonly XtreamBuildFileIdentity[];
|
||||
readonly sourceMapCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
const BACKEND_ROOT = 'dist/apps/electron-backend';
|
||||
const RENDERER_ROOT = 'dist/apps/web';
|
||||
const ELECTRON_PATHS = {
|
||||
databaseWorker: `${BACKEND_ROOT}/workers/database.worker.js`,
|
||||
main: `${BACKEND_ROOT}/main.js`,
|
||||
playlistRefreshWorker: `${BACKEND_ROOT}/workers/playlist-refresh.worker.js`,
|
||||
preload: `${BACKEND_ROOT}/main.preload.js`,
|
||||
} as const;
|
||||
|
||||
export async function captureXtreamBuildIdentity(
|
||||
workspaceRoot: string
|
||||
): Promise<XtreamBenchmarkBuildIdentity> {
|
||||
try {
|
||||
if (!isAbsolute(workspaceRoot)) invalid();
|
||||
const [databaseWorker, main, playlistRefreshWorker, preload, renderer] =
|
||||
await Promise.all([
|
||||
readPair(workspaceRoot, ELECTRON_PATHS.databaseWorker),
|
||||
readPair(workspaceRoot, ELECTRON_PATHS.main),
|
||||
readPair(workspaceRoot, ELECTRON_PATHS.playlistRefreshWorker),
|
||||
readPair(workspaceRoot, ELECTRON_PATHS.preload),
|
||||
readRenderer(workspaceRoot),
|
||||
]);
|
||||
return Object.freeze({
|
||||
electron: Object.freeze({
|
||||
databaseWorker,
|
||||
main,
|
||||
playlistRefreshWorker,
|
||||
preload,
|
||||
}),
|
||||
renderer,
|
||||
});
|
||||
} catch {
|
||||
throw new Error('invalid Xtream performance build');
|
||||
}
|
||||
}
|
||||
|
||||
export function computeXtreamBuildFilesAggregate(
|
||||
files: readonly XtreamBuildFileIdentity[]
|
||||
): string {
|
||||
try {
|
||||
const length = Reflect.get(files, 'length') as unknown;
|
||||
if (
|
||||
!Number.isSafeInteger(length) ||
|
||||
Number(length) <= 0 ||
|
||||
Number(length) > 10_000
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const snapshot = Array.from({ length: Number(length) }, (_, index) => {
|
||||
const value = Reflect.get(files, String(index)) as
|
||||
XtreamBuildFileIdentity | undefined;
|
||||
if (
|
||||
!value ||
|
||||
!Number.isSafeInteger(value.bytes) ||
|
||||
value.bytes < 0 ||
|
||||
typeof value.path !== 'string' ||
|
||||
value.path.length === 0 ||
|
||||
typeof value.sha256 !== 'string' ||
|
||||
!/^[a-f0-9]{64}$/u.test(value.sha256)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return [value.path, value.bytes, value.sha256] as const;
|
||||
});
|
||||
return sha256(Buffer.from(JSON.stringify(snapshot), 'utf8'));
|
||||
} catch {
|
||||
throw new Error('invalid Xtream build file identity');
|
||||
}
|
||||
}
|
||||
|
||||
async function readPair(
|
||||
workspaceRoot: string,
|
||||
javascriptPath: string
|
||||
): Promise<XtreamBuildPairIdentity> {
|
||||
const [javascript, sourceMap] = await Promise.all([
|
||||
readIdentity(workspaceRoot, javascriptPath),
|
||||
readIdentity(workspaceRoot, `${javascriptPath}.map`),
|
||||
]);
|
||||
return Object.freeze({ javascript, sourceMap });
|
||||
}
|
||||
|
||||
async function readRenderer(
|
||||
workspaceRoot: string
|
||||
): Promise<XtreamBenchmarkBuildIdentity['renderer']> {
|
||||
const rendererDirectory = join(workspaceRoot, RENDERER_ROOT);
|
||||
const namesBefore = await readRendererNames(rendererDirectory);
|
||||
assertRendererPairing(namesBefore);
|
||||
const relativePaths = namesBefore.map((name) => `${RENDERER_ROOT}/${name}`);
|
||||
const files = await Promise.all(
|
||||
relativePaths.map((path) => readIdentity(workspaceRoot, path))
|
||||
);
|
||||
const namesAfter = await readRendererNames(rendererDirectory);
|
||||
if (
|
||||
namesAfter.length !== namesBefore.length ||
|
||||
namesAfter.some((name, index) => name !== namesBefore[index])
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const executableJavaScriptCount = namesBefore.filter((name) =>
|
||||
name.endsWith('.js')
|
||||
).length;
|
||||
const sourceMapCount = namesBefore.filter((name) =>
|
||||
name.endsWith('.js.map')
|
||||
).length;
|
||||
const bytes = files.reduce((total, file) => total + file.bytes, 0);
|
||||
if (!Number.isSafeInteger(bytes) || bytes <= 0) invalid();
|
||||
const frozenFiles = Object.freeze(files);
|
||||
return Object.freeze({
|
||||
aggregateSha256: computeXtreamBuildFilesAggregate(frozenFiles),
|
||||
bytes,
|
||||
executableJavaScriptCount,
|
||||
files: frozenFiles,
|
||||
sourceMapCount,
|
||||
});
|
||||
}
|
||||
|
||||
async function readRendererNames(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const relevant = entries.filter(
|
||||
(entry) =>
|
||||
entry.name === 'index.html' ||
|
||||
entry.name.endsWith('.js') ||
|
||||
entry.name.endsWith('.js.map')
|
||||
);
|
||||
if (
|
||||
relevant.some((entry) => !entry.isFile()) ||
|
||||
relevant.filter((entry) => entry.name === 'index.html').length !== 1
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return relevant.map((entry) => entry.name).sort();
|
||||
}
|
||||
|
||||
function assertRendererPairing(names: readonly string[]): void {
|
||||
const javascript = names.filter((name) => name.endsWith('.js'));
|
||||
const sourceMaps = names.filter((name) => name.endsWith('.js.map'));
|
||||
const javascriptSet = new Set(javascript);
|
||||
const sourceMapSet = new Set(sourceMaps);
|
||||
if (
|
||||
javascript.length === 0 ||
|
||||
javascript.length !== sourceMaps.length ||
|
||||
javascript.some((name) => !sourceMapSet.has(`${name}.map`)) ||
|
||||
sourceMaps.some((name) => !javascriptSet.has(name.slice(0, -4)))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
async function readIdentity(
|
||||
workspaceRoot: string,
|
||||
relativePath: string
|
||||
): Promise<XtreamBuildFileIdentity> {
|
||||
const { content } = await readXtreamSafeRegularFile(
|
||||
join(workspaceRoot, relativePath)
|
||||
);
|
||||
return Object.freeze({
|
||||
bytes: content.byteLength,
|
||||
path: relativePath,
|
||||
sha256: sha256(content),
|
||||
});
|
||||
}
|
||||
|
||||
function sha256(value: Uint8Array): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import type { DatabaseWorkerCancelTimelineRecord } from './database-worker-cancel-capture';
|
||||
import { snapshotXtreamCancellationEvidence } from './xtream-cancellation-evidence-snapshot';
|
||||
import {
|
||||
isFiniteNonNegative,
|
||||
isPositiveSafeInteger,
|
||||
} from './xtream-exact-schema';
|
||||
import type {
|
||||
XtreamAttributionPhaseEvent,
|
||||
XtreamIpcAttributionSpan,
|
||||
} from './xtream-phase-attribution';
|
||||
import {
|
||||
hasDistinctMonotonicXtreamIpcProducerIdentities,
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE,
|
||||
xtreamIpcProducerNamespaceForWorkerOperation,
|
||||
type XtreamIpcProducerIdentity,
|
||||
} from './xtream-ipc-producer-identity';
|
||||
import { XTREAM_ATTRIBUTION_PHASE as PHASE } from './xtream-phase-inventory';
|
||||
import type {
|
||||
XtreamCancellationMetrics,
|
||||
XtreamRawIterationResult,
|
||||
} from './xtream-summary-contract';
|
||||
|
||||
type WorkerRequest =
|
||||
XtreamRawIterationResult['databaseWorker']['requests']['evidence'][number];
|
||||
|
||||
const RECORD_TYPES = [
|
||||
'db-cancel-dispatched',
|
||||
'db-cancel-received',
|
||||
'db-cancel-terminal-received',
|
||||
] as const;
|
||||
|
||||
export function assertXtreamCancellationEvidenceBinding(
|
||||
raw: XtreamRawIterationResult,
|
||||
events: readonly XtreamAttributionPhaseEvent[],
|
||||
ipcSpans: readonly XtreamIpcAttributionSpan[],
|
||||
requests: readonly WorkerRequest[]
|
||||
): void {
|
||||
const failed = requests.filter(
|
||||
({ operation, success }) => operation === 'DB_SAVE_CONTENT' && !success
|
||||
);
|
||||
if (raw.scenarioId !== 'xtream-cancel-import') {
|
||||
if (failed.length !== 0 || raw.cancellationEvidence !== null) invalid();
|
||||
return;
|
||||
}
|
||||
const request = failed[0];
|
||||
if (
|
||||
failed.length !== 1 ||
|
||||
!request ||
|
||||
typeof request.requestId !== 'string' ||
|
||||
typeof request.operationId !== 'string' ||
|
||||
raw.cancellation.failedDatabaseRequestId !== request.requestId ||
|
||||
raw.cancellation.failedOperationId !== request.operationId
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const timeline = cancellationTimeline(raw, request);
|
||||
const databasePreload = ipcPair(ipcSpans, 'dbCancelOperation');
|
||||
assertCancellationProducerIdentity(timeline, databasePreload[0], requests);
|
||||
assertCancellationTimeline(
|
||||
raw.cancellation,
|
||||
request,
|
||||
timeline,
|
||||
phaseSpan(
|
||||
events,
|
||||
PHASE.SQLITE_CONTENT_WRITE_TRANSACTIONS,
|
||||
request.requestId
|
||||
),
|
||||
phaseSpan(events, PHASE.CANCEL_SESSION),
|
||||
phaseSpan(events, PHASE.STORE_IMPORT_TERMINAL),
|
||||
ipcPair(ipcSpans, 'xtreamCancelSession'),
|
||||
databasePreload,
|
||||
raw.phaseCapture.uiPaintEpochMs
|
||||
);
|
||||
}
|
||||
|
||||
interface Span {
|
||||
readonly endEpochMs: number;
|
||||
readonly startEpochMs: number;
|
||||
}
|
||||
|
||||
function assertCancellationTimeline(
|
||||
value: XtreamCancellationMetrics,
|
||||
request: WorkerRequest,
|
||||
timeline: readonly DatabaseWorkerCancelTimelineRecord[],
|
||||
write: Span,
|
||||
networkCancel: Span,
|
||||
terminal: Span,
|
||||
networkPreload: readonly [
|
||||
XtreamIpcAttributionSpan,
|
||||
XtreamIpcAttributionSpan,
|
||||
],
|
||||
databasePreload: readonly [
|
||||
XtreamIpcAttributionSpan,
|
||||
XtreamIpcAttributionSpan,
|
||||
],
|
||||
uiPaintEpochMs: number
|
||||
): void {
|
||||
const [dispatchRecord, receiptRecord, workerTerminalRecord] = timeline;
|
||||
if (!dispatchRecord || !receiptRecord || !workerTerminalRecord) invalid();
|
||||
const click = epoch(value.clickEpochMs);
|
||||
const preloadStart = epoch(value.preloadStartEpochMs);
|
||||
const preloadReturn = epoch(value.preloadReturnEpochMs);
|
||||
const dispatch = epoch(value.databaseDispatchEpochMs);
|
||||
const receipt = epoch(value.workerReceiptEpochMs);
|
||||
const authoritative = epoch(value.authoritativeTerminalEpochMs);
|
||||
const painted = epoch(value.paintedEpochMs);
|
||||
if (
|
||||
request.requestReceivedEpochMs === null ||
|
||||
request.workEndedEpochMs === null ||
|
||||
click < write.startEpochMs ||
|
||||
click < request.requestReceivedEpochMs ||
|
||||
click > write.endEpochMs ||
|
||||
networkPreload[0].startEpochMs < click ||
|
||||
networkPreload[0].endEpochMs > networkCancel.startEpochMs ||
|
||||
networkCancel.endEpochMs > networkPreload[1].startEpochMs ||
|
||||
networkPreload[1].startEpochMs > databasePreload[0].startEpochMs ||
|
||||
preloadStart !== databasePreload[0].startEpochMs ||
|
||||
dispatchRecord.ipcCallId !== databasePreload[0].ipcCallId ||
|
||||
dispatchRecord.sourceEpochMs !== databasePreload[0].sourceEpochMs ||
|
||||
databasePreload[0].endEpochMs > dispatch ||
|
||||
dispatch !== dispatchRecord.epochMs ||
|
||||
dispatch < request.requestReceivedEpochMs ||
|
||||
dispatch > write.endEpochMs ||
|
||||
databasePreload[1].startEpochMs < dispatch ||
|
||||
preloadReturn !== databasePreload[1].endEpochMs ||
|
||||
preloadReturn < dispatch ||
|
||||
receipt !== receiptRecord.epochMs ||
|
||||
receipt < dispatch ||
|
||||
receipt > request.workEndedEpochMs ||
|
||||
receipt > authoritative ||
|
||||
workerTerminalRecord.epochMs < receipt ||
|
||||
workerTerminalRecord.epochMs > authoritative ||
|
||||
authoritative !== request.responseEpochMs ||
|
||||
terminal.startEpochMs < authoritative ||
|
||||
terminal.endEpochMs > painted ||
|
||||
painted !== uiPaintEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function cancellationTimeline(
|
||||
raw: XtreamRawIterationResult,
|
||||
request: WorkerRequest
|
||||
): readonly DatabaseWorkerCancelTimelineRecord[] {
|
||||
const evidence = snapshotXtreamCancellationEvidence(
|
||||
raw.cancellationEvidence
|
||||
);
|
||||
if (evidence === null) invalid();
|
||||
const timeline = evidence.timeline;
|
||||
if (timeline.length !== RECORD_TYPES.length) invalid();
|
||||
const first = timeline[0];
|
||||
if (!first) invalid();
|
||||
for (let index = 0; index < timeline.length; index += 1) {
|
||||
const record = timeline[index];
|
||||
if (
|
||||
!record ||
|
||||
record.type !== RECORD_TYPES[index] ||
|
||||
!isFiniteNonNegative(record.epochMs) ||
|
||||
!isFiniteNonNegative(record.sourceEpochMs) ||
|
||||
!isPositiveSafeInteger(record.ipcCallId) ||
|
||||
typeof record.operationId !== 'string' ||
|
||||
record.operationId.length === 0 ||
|
||||
typeof record.requestId !== 'string' ||
|
||||
record.requestId.length === 0 ||
|
||||
record.ipcCallId !== first.ipcCallId ||
|
||||
record.operationId !== first.operationId ||
|
||||
record.requestId !== first.requestId ||
|
||||
record.sourceEpochMs !== first.sourceEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
if (
|
||||
first.requestId !== request.requestId ||
|
||||
first.operationId !== request.operationId
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return timeline;
|
||||
}
|
||||
|
||||
function assertCancellationProducerIdentity(
|
||||
timeline: readonly DatabaseWorkerCancelTimelineRecord[],
|
||||
databasePreload: XtreamIpcAttributionSpan,
|
||||
requests: readonly WorkerRequest[]
|
||||
): void {
|
||||
const dispatch = timeline[0];
|
||||
if (
|
||||
!dispatch ||
|
||||
dispatch.ipcCallId !== databasePreload.ipcCallId ||
|
||||
dispatch.sourceEpochMs !== databasePreload.sourceEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const identities: XtreamIpcProducerIdentity[] = [];
|
||||
for (const { ipcCallId, operation, sourceEpochMs } of requests) {
|
||||
if (ipcCallId === null && sourceEpochMs === null) continue;
|
||||
const namespace = xtreamIpcProducerNamespaceForWorkerOperation(
|
||||
typeof operation === 'string' ? operation : ''
|
||||
);
|
||||
if (
|
||||
namespace === null ||
|
||||
!isPositiveSafeInteger(ipcCallId) ||
|
||||
!isFiniteNonNegative(sourceEpochMs)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
if (namespace === XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD) {
|
||||
identities.push({ ipcCallId, namespace, sourceEpochMs });
|
||||
}
|
||||
}
|
||||
identities.push({
|
||||
ipcCallId: databasePreload.ipcCallId,
|
||||
namespace: XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD,
|
||||
sourceEpochMs: databasePreload.sourceEpochMs,
|
||||
});
|
||||
if (!hasDistinctMonotonicXtreamIpcProducerIdentities(identities)) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function phaseSpan(
|
||||
events: readonly XtreamAttributionPhaseEvent[],
|
||||
phase: string,
|
||||
correlationId?: string
|
||||
): Span {
|
||||
const selected = events.filter(
|
||||
(event) =>
|
||||
event.phase === phase &&
|
||||
(correlationId === undefined ||
|
||||
event.correlationId === correlationId)
|
||||
);
|
||||
const start = selected.find(({ boundary }) => boundary === 'start');
|
||||
const end = selected.find(({ boundary }) => boundary === 'end');
|
||||
if (selected.length !== 2 || !start || !end) invalid();
|
||||
return { endEpochMs: end.epochMs, startEpochMs: start.epochMs };
|
||||
}
|
||||
|
||||
function ipcPair(
|
||||
spans: readonly XtreamIpcAttributionSpan[],
|
||||
method: string
|
||||
): readonly [XtreamIpcAttributionSpan, XtreamIpcAttributionSpan] {
|
||||
const selected = spans.filter((span) => span.method === method);
|
||||
const request = selected.find(({ boundary }) => boundary === 'request');
|
||||
const response = selected.find(({ boundary }) => boundary === 'response');
|
||||
if (selected.length !== 2 || !request || !response) invalid();
|
||||
return [request, response];
|
||||
}
|
||||
|
||||
function epoch(value: number | null): number {
|
||||
if (!isFiniteNonNegative(value)) invalid();
|
||||
return value;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid Xtream persisted cancellation evidence');
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { DatabaseWorkerCancelTimelineRecord } from './database-worker-cancel-capture';
|
||||
import type { XtreamCancellationEvidence } from './xtream-summary-contract';
|
||||
|
||||
const EVIDENCE_KEYS = ['timeline'] as const;
|
||||
const RECORD_KEYS = [
|
||||
'epochMs',
|
||||
'ipcCallId',
|
||||
'operationId',
|
||||
'requestId',
|
||||
'sourceEpochMs',
|
||||
'type',
|
||||
] as const;
|
||||
const MAX_TIMELINE_LENGTH = 16;
|
||||
|
||||
export function snapshotXtreamCancellationEvidence(
|
||||
value: unknown
|
||||
): XtreamCancellationEvidence | null {
|
||||
try {
|
||||
const evidence = snapshotExactRecord(value, EVIDENCE_KEYS);
|
||||
const timeline = snapshotExactArray(
|
||||
evidence['timeline'],
|
||||
snapshotTimelineRecord
|
||||
);
|
||||
return Object.freeze({ timeline });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotTimelineRecord(
|
||||
value: unknown
|
||||
): DatabaseWorkerCancelTimelineRecord {
|
||||
return snapshotExactRecord(
|
||||
value,
|
||||
RECORD_KEYS
|
||||
) as unknown as DatabaseWorkerCancelTimelineRecord;
|
||||
}
|
||||
|
||||
function snapshotExactArray<T>(
|
||||
value: unknown,
|
||||
snapshotEntry: (entry: unknown) => T
|
||||
): readonly T[] {
|
||||
if (!Array.isArray(value)) invalid();
|
||||
const length = dataDescriptorValue(
|
||||
Reflect.getOwnPropertyDescriptor(value, 'length')
|
||||
);
|
||||
if (
|
||||
!Number.isSafeInteger(length) ||
|
||||
Number(length) < 0 ||
|
||||
Number(length) > MAX_TIMELINE_LENGTH
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const expectedKeys = new Set<PropertyKey>(['length']);
|
||||
const snapshot: T[] = [];
|
||||
for (let index = 0; index < Number(length); index += 1) {
|
||||
const key = String(index);
|
||||
expectedKeys.add(key);
|
||||
snapshot[index] = snapshotEntry(
|
||||
dataDescriptorValue(Reflect.getOwnPropertyDescriptor(value, key))
|
||||
);
|
||||
}
|
||||
const ownKeys = Reflect.ownKeys(value);
|
||||
if (
|
||||
ownKeys.length !== expectedKeys.size ||
|
||||
ownKeys.some((key) => !expectedKeys.has(key))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.freeze(snapshot);
|
||||
}
|
||||
|
||||
function snapshotExactRecord(
|
||||
value: unknown,
|
||||
keys: readonly string[]
|
||||
): Readonly<Record<string, unknown>> {
|
||||
if (
|
||||
typeof value !== 'object' ||
|
||||
value === null ||
|
||||
Array.isArray(value) ||
|
||||
![Object.prototype, null].includes(Reflect.getPrototypeOf(value))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const ownKeys = Reflect.ownKeys(value);
|
||||
if (
|
||||
ownKeys.length !== keys.length ||
|
||||
ownKeys.some((key) => typeof key !== 'string' || !keys.includes(key))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const snapshot: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
snapshot[key] = dataDescriptorValue(
|
||||
Reflect.getOwnPropertyDescriptor(value, key)
|
||||
);
|
||||
}
|
||||
return Object.freeze(snapshot);
|
||||
}
|
||||
|
||||
function dataDescriptorValue(
|
||||
descriptor: PropertyDescriptor | undefined
|
||||
): unknown {
|
||||
if (
|
||||
!descriptor ||
|
||||
!Object.prototype.hasOwnProperty.call(descriptor, 'value')
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return descriptor.value;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid Xtream cancellation evidence snapshot');
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import type { WorkerRequestPerformanceMetrics } from './m3u-refresh-cancellation-contract';
|
||||
import type { DatabaseWorkerCancelTimelineRecord } from './database-worker-cancel-capture';
|
||||
import type {
|
||||
XtreamAttributionPhaseEvent,
|
||||
XtreamIpcAttributionSpan,
|
||||
XtreamPhaseAttributionInput,
|
||||
} from './xtream-phase-attribution';
|
||||
import {
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE,
|
||||
xtreamIpcProducerNamespaceForMethod,
|
||||
} from './xtream-ipc-producer-identity';
|
||||
import { XTREAM_ATTRIBUTION_PHASE as PHASE } from './xtream-phase-inventory';
|
||||
import type { XtreamCancellationMetrics } from './xtream-summary-contract';
|
||||
import type { XtreamCancellationEvidence } from './xtream-summary-contract';
|
||||
|
||||
export function bindFixtureCancellationEvidence(
|
||||
input: XtreamPhaseAttributionInput,
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
): {
|
||||
readonly cancellation: XtreamCancellationMetrics;
|
||||
readonly cancellationEvidence: XtreamCancellationEvidence | null;
|
||||
readonly phaseCapture: XtreamPhaseAttributionInput;
|
||||
} {
|
||||
if (input.scenarioId !== 'xtream-cancel-import') {
|
||||
return {
|
||||
cancellation: emptyCancellation(),
|
||||
cancellationEvidence: null,
|
||||
phaseCapture: input,
|
||||
};
|
||||
}
|
||||
const failed = requests.filter(
|
||||
({ operation, success }) => operation === 'DB_SAVE_CONTENT' && !success
|
||||
);
|
||||
const request = failed[0];
|
||||
if (
|
||||
failed.length !== 1 ||
|
||||
!request ||
|
||||
typeof request.requestId !== 'string' ||
|
||||
typeof request.operationId !== 'string' ||
|
||||
request.workEndedEpochMs === null
|
||||
) {
|
||||
fixtureError();
|
||||
}
|
||||
const write = phaseSpan(
|
||||
input.events,
|
||||
PHASE.SQLITE_CONTENT_WRITE_TRANSACTIONS,
|
||||
request.requestId
|
||||
);
|
||||
const cancel = phaseSpan(input.events, PHASE.CANCEL_SESSION);
|
||||
const clickEpochMs = write.startEpochMs + 0.25;
|
||||
const networkPreloadStartEpochMs = clickEpochMs + 0.25;
|
||||
const networkPreloadReturnEpochMs = cancel.endEpochMs + 0.25;
|
||||
const preloadStartEpochMs = networkPreloadReturnEpochMs + 0.25;
|
||||
const databaseDispatchEpochMs = preloadStartEpochMs + 0.5;
|
||||
const preloadReturnEpochMs = databaseDispatchEpochMs + 0.5;
|
||||
const workerReceiptEpochMs = preloadReturnEpochMs + 0.25;
|
||||
const workerTerminalEpochMs = workerReceiptEpochMs + 0.25;
|
||||
if (
|
||||
databaseDispatchEpochMs > write.endEpochMs ||
|
||||
workerTerminalEpochMs > request.workEndedEpochMs ||
|
||||
workerTerminalEpochMs > request.responseEpochMs
|
||||
) {
|
||||
fixtureError();
|
||||
}
|
||||
const authoritativeTerminalEpochMs = request.responseEpochMs;
|
||||
const paintedEpochMs = input.uiPaintEpochMs;
|
||||
const ipcSpans = resequenceFixtureIpcSpans(
|
||||
input.ipcSpans.map((span) => {
|
||||
if (span.method === 'xtreamCancelSession') {
|
||||
return retime(
|
||||
span,
|
||||
networkPreloadStartEpochMs,
|
||||
cancel.startEpochMs,
|
||||
cancel.endEpochMs,
|
||||
networkPreloadReturnEpochMs
|
||||
);
|
||||
}
|
||||
if (span.method === 'dbCancelOperation') {
|
||||
return retime(
|
||||
span,
|
||||
preloadStartEpochMs,
|
||||
databaseDispatchEpochMs - 0.25,
|
||||
databaseDispatchEpochMs + 0.25,
|
||||
preloadReturnEpochMs
|
||||
);
|
||||
}
|
||||
return span;
|
||||
})
|
||||
);
|
||||
const databasePreload = ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbCancelOperation'
|
||||
);
|
||||
if (!databasePreload) fixtureError();
|
||||
const cancelTimeline = cancellationTimeline(
|
||||
request,
|
||||
databasePreload,
|
||||
databaseDispatchEpochMs,
|
||||
workerReceiptEpochMs,
|
||||
workerTerminalEpochMs
|
||||
);
|
||||
return {
|
||||
cancellation: {
|
||||
acknowledgementLatencyMs:
|
||||
authoritativeTerminalEpochMs - clickEpochMs,
|
||||
authoritativeDatabaseAcknowledgementObserved: true,
|
||||
authoritativeTerminalEpochMs,
|
||||
clickEpochMs,
|
||||
clickToDatabaseDispatchMs: databaseDispatchEpochMs - clickEpochMs,
|
||||
clickToPaintMs: paintedEpochMs - clickEpochMs,
|
||||
clickToPreloadReturnMs: preloadReturnEpochMs - clickEpochMs,
|
||||
databaseDispatchEpochMs,
|
||||
failedDatabaseRequestId: request.requestId,
|
||||
failedOperationId: request.operationId,
|
||||
mainNetworkAbortObserved: false,
|
||||
mainNetworkAbortReason: 'cancel-triggered-after-network-complete',
|
||||
paintedEpochMs,
|
||||
preloadReturnEpochMs,
|
||||
preloadStartEpochMs,
|
||||
workerReceiptEpochMs,
|
||||
workerReceiptIsAuthoritative: false,
|
||||
workerReceiptLatencyMs: workerReceiptEpochMs - clickEpochMs,
|
||||
},
|
||||
cancellationEvidence: { timeline: cancelTimeline },
|
||||
phaseCapture: { ...input, ipcSpans },
|
||||
};
|
||||
}
|
||||
|
||||
function cancellationTimeline(
|
||||
request: WorkerRequestPerformanceMetrics,
|
||||
databasePreload: XtreamIpcAttributionSpan,
|
||||
dispatchEpochMs: number,
|
||||
receiptEpochMs: number,
|
||||
terminalEpochMs: number
|
||||
): readonly DatabaseWorkerCancelTimelineRecord[] {
|
||||
if (
|
||||
typeof request.requestId !== 'string' ||
|
||||
typeof request.operationId !== 'string'
|
||||
) {
|
||||
fixtureError();
|
||||
}
|
||||
const identity = {
|
||||
ipcCallId: databasePreload.ipcCallId,
|
||||
operationId: request.operationId,
|
||||
requestId: request.requestId,
|
||||
sourceEpochMs: databasePreload.sourceEpochMs,
|
||||
};
|
||||
return [
|
||||
{
|
||||
...identity,
|
||||
epochMs: dispatchEpochMs,
|
||||
type: 'db-cancel-dispatched',
|
||||
},
|
||||
{
|
||||
...identity,
|
||||
epochMs: receiptEpochMs,
|
||||
type: 'db-cancel-received',
|
||||
},
|
||||
{
|
||||
...identity,
|
||||
epochMs: terminalEpochMs,
|
||||
type: 'db-cancel-terminal-received',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function emptyCancellation(): XtreamCancellationMetrics {
|
||||
return {
|
||||
acknowledgementLatencyMs: null,
|
||||
authoritativeDatabaseAcknowledgementObserved: false,
|
||||
authoritativeTerminalEpochMs: null,
|
||||
clickEpochMs: null,
|
||||
clickToDatabaseDispatchMs: null,
|
||||
clickToPaintMs: null,
|
||||
clickToPreloadReturnMs: null,
|
||||
databaseDispatchEpochMs: null,
|
||||
failedDatabaseRequestId: null,
|
||||
failedOperationId: null,
|
||||
mainNetworkAbortObserved: false,
|
||||
mainNetworkAbortReason: null,
|
||||
paintedEpochMs: null,
|
||||
preloadReturnEpochMs: null,
|
||||
preloadStartEpochMs: null,
|
||||
workerReceiptEpochMs: null,
|
||||
workerReceiptIsAuthoritative: false,
|
||||
workerReceiptLatencyMs: null,
|
||||
};
|
||||
}
|
||||
|
||||
function retime(
|
||||
span: XtreamIpcAttributionSpan,
|
||||
requestStartEpochMs: number,
|
||||
requestEndEpochMs: number,
|
||||
responseStartEpochMs: number,
|
||||
responseEndEpochMs: number
|
||||
): XtreamIpcAttributionSpan {
|
||||
return {
|
||||
...span,
|
||||
endEpochMs:
|
||||
span.boundary === 'request'
|
||||
? requestEndEpochMs
|
||||
: responseEndEpochMs,
|
||||
startEpochMs:
|
||||
span.boundary === 'request'
|
||||
? requestStartEpochMs
|
||||
: responseStartEpochMs,
|
||||
sourceEpochMs: requestStartEpochMs,
|
||||
};
|
||||
}
|
||||
|
||||
function resequenceFixtureIpcSpans(
|
||||
spans: readonly XtreamIpcAttributionSpan[]
|
||||
): readonly XtreamIpcAttributionSpan[] {
|
||||
const groups = new Map<string, XtreamIpcAttributionSpan[]>();
|
||||
for (const span of spans) {
|
||||
groups.set(span.correlationId, [
|
||||
...(groups.get(span.correlationId) ?? []),
|
||||
span,
|
||||
]);
|
||||
}
|
||||
const calls = [...groups.entries()].map(([correlationId, call]) => {
|
||||
const span = call[0];
|
||||
if (
|
||||
call.length !== 2 ||
|
||||
!span ||
|
||||
call.some(
|
||||
(candidate) =>
|
||||
candidate.ipcCallId !== span.ipcCallId ||
|
||||
candidate.sourceEpochMs !== span.sourceEpochMs
|
||||
)
|
||||
) {
|
||||
fixtureError();
|
||||
}
|
||||
const namespace = xtreamIpcProducerNamespaceForMethod(span.method);
|
||||
if (namespace === null) fixtureError();
|
||||
return { correlationId, namespace, span };
|
||||
});
|
||||
const reassigned = new Map<string, number>();
|
||||
for (const namespace of Object.values(XTREAM_IPC_PRODUCER_NAMESPACE)) {
|
||||
const selected = calls
|
||||
.filter((call) => call.namespace === namespace)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.span.sourceEpochMs - right.span.sourceEpochMs ||
|
||||
left.span.ipcCallId - right.span.ipcCallId
|
||||
);
|
||||
const ids = selected
|
||||
.map(({ span }) => span.ipcCallId)
|
||||
.sort((left, right) => left - right);
|
||||
if (new Set(ids).size !== ids.length) fixtureError();
|
||||
selected.forEach(({ correlationId }, index) => {
|
||||
const ipcCallId = ids[index];
|
||||
if (!ipcCallId) fixtureError();
|
||||
reassigned.set(correlationId, ipcCallId);
|
||||
});
|
||||
}
|
||||
return spans.map((span) => {
|
||||
const ipcCallId = reassigned.get(span.correlationId);
|
||||
if (!ipcCallId) fixtureError();
|
||||
return { ...span, ipcCallId };
|
||||
});
|
||||
}
|
||||
|
||||
function phaseSpan(
|
||||
events: readonly XtreamAttributionPhaseEvent[],
|
||||
phase: string,
|
||||
correlationId?: string
|
||||
): { readonly endEpochMs: number; readonly startEpochMs: number } {
|
||||
const selected = events.filter(
|
||||
(event) =>
|
||||
event.phase === phase &&
|
||||
(correlationId === undefined ||
|
||||
event.correlationId === correlationId)
|
||||
);
|
||||
const start = selected.find(({ boundary }) => boundary === 'start');
|
||||
const end = selected.find(({ boundary }) => boundary === 'end');
|
||||
if (selected.length !== 2 || !start || !end) fixtureError();
|
||||
return { endEpochMs: end.epochMs, startEpochMs: start.epochMs };
|
||||
}
|
||||
|
||||
function fixtureError(): never {
|
||||
throw new Error('invalid Xtream cancellation fixture evidence');
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
export interface XtreamCatalogDatabaseQuery {
|
||||
readonly parameters: readonly (number | string)[];
|
||||
readonly sql: string;
|
||||
}
|
||||
|
||||
const CATEGORY_PARTITIONS = [
|
||||
{
|
||||
categoryType: 'live',
|
||||
expectedName: 'Performance Live %02d',
|
||||
idBase: 91_100,
|
||||
},
|
||||
{
|
||||
categoryType: 'movies',
|
||||
expectedName: 'Performance VOD %02d',
|
||||
idBase: 92_100,
|
||||
},
|
||||
{
|
||||
categoryType: 'series',
|
||||
expectedName: 'Performance Series %02d',
|
||||
idBase: 93_100,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const CONTENT_PARTITIONS = [
|
||||
{
|
||||
categoryType: 'live',
|
||||
contentType: 'live',
|
||||
expectedTitle: 'Performance Live %06d',
|
||||
idBase: 999_999,
|
||||
},
|
||||
{
|
||||
categoryType: 'movies',
|
||||
contentType: 'movie',
|
||||
expectedTitle: 'Performance Movie %06d',
|
||||
idBase: 1_999_999,
|
||||
},
|
||||
{
|
||||
categoryType: 'series',
|
||||
contentType: 'series',
|
||||
expectedTitle: 'Performance Series %06d',
|
||||
idBase: 2_999_999,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const PLAYLIST_QUERY = `
|
||||
SELECT
|
||||
COUNT(*) AS playlistCount,
|
||||
COALESCE(SUM(CASE WHEN id = ? THEN 1 ELSE 0 END), 0)
|
||||
AS matchingPlaylistCount,
|
||||
MAX(CASE WHEN id = ? THEN type ELSE NULL END)
|
||||
AS matchingPlaylistType
|
||||
FROM playlists
|
||||
`;
|
||||
|
||||
const CATEGORY_COUNT_QUERY = `
|
||||
SELECT COUNT(*) AS categoryCount
|
||||
FROM categories
|
||||
WHERE playlist_id = ?
|
||||
`;
|
||||
|
||||
const CONTENT_COUNT_QUERY = `
|
||||
SELECT COUNT(*) AS contentCount
|
||||
FROM content AS item
|
||||
INNER JOIN categories AS category ON category.id = item.category_id
|
||||
WHERE category.playlist_id = ?
|
||||
`;
|
||||
|
||||
const CATEGORY_PARTITION_QUERY = `
|
||||
SELECT
|
||||
COUNT(*) AS count,
|
||||
COUNT(DISTINCT xtream_id) AS distinctXtreamIdCount,
|
||||
COALESCE(
|
||||
SUM(CASE
|
||||
WHEN name <> printf(?, xtream_id - ?) THEN 1
|
||||
ELSE 0
|
||||
END),
|
||||
0
|
||||
) AS invalidNameCount,
|
||||
MAX(xtream_id) AS maxXtreamId,
|
||||
MIN(xtream_id) AS minXtreamId
|
||||
FROM categories
|
||||
WHERE playlist_id = ? AND type = ?
|
||||
`;
|
||||
|
||||
const CONTENT_PARTITION_QUERY = `
|
||||
SELECT
|
||||
COUNT(*) AS count,
|
||||
COUNT(DISTINCT item.xtream_id) AS distinctXtreamIdCount,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM (
|
||||
SELECT expected_category.id
|
||||
FROM categories AS expected_category
|
||||
LEFT JOIN content AS expected_item
|
||||
ON expected_item.category_id = expected_category.id
|
||||
AND expected_item.type = ?
|
||||
WHERE expected_category.playlist_id = ?
|
||||
AND expected_category.type = ?
|
||||
GROUP BY expected_category.id
|
||||
HAVING COUNT(expected_item.id) <> 1000
|
||||
)
|
||||
) AS invalidCategoryCardinalityCount,
|
||||
COALESCE(
|
||||
SUM(CASE
|
||||
WHEN item.title <> printf(?, item.xtream_id - ?) THEN 1
|
||||
ELSE 0
|
||||
END),
|
||||
0
|
||||
) AS invalidTitleCount,
|
||||
MAX(item.xtream_id) AS maxXtreamId,
|
||||
MIN(item.xtream_id) AS minXtreamId
|
||||
FROM content AS item
|
||||
INNER JOIN categories AS category ON category.id = item.category_id
|
||||
WHERE category.playlist_id = ?
|
||||
AND category.type = ?
|
||||
AND item.type = ?
|
||||
`;
|
||||
|
||||
const IMPORT_STATUS_QUERY = `
|
||||
SELECT
|
||||
(SELECT value FROM app_state WHERE key = ?) AS live,
|
||||
(SELECT value FROM app_state WHERE key = ?) AS movie,
|
||||
(SELECT value FROM app_state WHERE key = ?) AS series
|
||||
`;
|
||||
|
||||
export function buildXtreamCatalogDatabaseQueries(
|
||||
playlistId: string
|
||||
): readonly XtreamCatalogDatabaseQuery[] {
|
||||
const queries: XtreamCatalogDatabaseQuery[] = [
|
||||
descriptor(PLAYLIST_QUERY, [playlistId, playlistId]),
|
||||
descriptor(CATEGORY_COUNT_QUERY, [playlistId]),
|
||||
descriptor(CONTENT_COUNT_QUERY, [playlistId]),
|
||||
];
|
||||
for (const partition of CATEGORY_PARTITIONS) {
|
||||
queries.push(
|
||||
descriptor(CATEGORY_PARTITION_QUERY, [
|
||||
partition.expectedName,
|
||||
partition.idBase,
|
||||
playlistId,
|
||||
partition.categoryType,
|
||||
])
|
||||
);
|
||||
}
|
||||
for (const partition of CONTENT_PARTITIONS) {
|
||||
queries.push(
|
||||
descriptor(CONTENT_PARTITION_QUERY, [
|
||||
partition.contentType,
|
||||
playlistId,
|
||||
partition.categoryType,
|
||||
partition.expectedTitle,
|
||||
partition.idBase,
|
||||
playlistId,
|
||||
partition.categoryType,
|
||||
partition.contentType,
|
||||
])
|
||||
);
|
||||
}
|
||||
queries.push(
|
||||
descriptor(IMPORT_STATUS_QUERY, [
|
||||
importStatusKey(playlistId, 'live'),
|
||||
importStatusKey(playlistId, 'movie'),
|
||||
importStatusKey(playlistId, 'series'),
|
||||
])
|
||||
);
|
||||
return Object.freeze(queries);
|
||||
}
|
||||
|
||||
function descriptor(
|
||||
sql: string,
|
||||
parameters: readonly (number | string)[]
|
||||
): XtreamCatalogDatabaseQuery {
|
||||
return Object.freeze({
|
||||
parameters: Object.freeze([...parameters]),
|
||||
sql,
|
||||
});
|
||||
}
|
||||
|
||||
function importStatusKey(playlistId: string, type: string): string {
|
||||
return `xtream-import-status:${playlistId}:${type}`;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
readXtreamCatalogDatabaseSnapshot,
|
||||
type XtreamCatalogDatabaseEvaluator,
|
||||
} from './xtream-catalog-database-reader';
|
||||
|
||||
interface TestStatement {
|
||||
get(...parameters: readonly (number | string)[]): unknown;
|
||||
run(...parameters: readonly (number | string)[]): unknown;
|
||||
}
|
||||
|
||||
interface TestDatabase {
|
||||
close(): void;
|
||||
exec(source: string): void;
|
||||
prepare(source: string): TestStatement;
|
||||
}
|
||||
|
||||
interface TestDatabaseConstructor {
|
||||
new (path: string): TestDatabase;
|
||||
}
|
||||
|
||||
interface RuntimeProcess {
|
||||
getBuiltinModule(id: string): unknown;
|
||||
}
|
||||
|
||||
const { DatabaseSync } = (
|
||||
process as unknown as RuntimeProcess
|
||||
).getBuiltinModule('node:sqlite') as {
|
||||
readonly DatabaseSync: TestDatabaseConstructor;
|
||||
};
|
||||
|
||||
describe('Xtream catalog database reader', () => {
|
||||
it('reads the scoped catalog and import state after capture stops', async () => {
|
||||
const database = createDatabase();
|
||||
seedPlaylist(database, 'playlist-1');
|
||||
seedPartition(database, {
|
||||
categoryId: 91_101,
|
||||
categoryName: 'Performance Live 01',
|
||||
categoryType: 'live',
|
||||
contentBase: 1_000_000,
|
||||
contentType: 'live',
|
||||
playlistId: 'playlist-1',
|
||||
titleLabel: 'Live',
|
||||
});
|
||||
seedPartition(database, {
|
||||
categoryId: 92_101,
|
||||
categoryName: 'Performance VOD 01',
|
||||
categoryType: 'movies',
|
||||
contentBase: 2_000_000,
|
||||
contentType: 'movie',
|
||||
playlistId: 'playlist-1',
|
||||
titleLabel: 'Movie',
|
||||
});
|
||||
seedPartition(database, {
|
||||
categoryId: 93_101,
|
||||
categoryName: 'Performance Series 01',
|
||||
categoryType: 'series',
|
||||
contentBase: 3_000_000,
|
||||
contentType: 'series',
|
||||
playlistId: 'playlist-1',
|
||||
titleLabel: 'Series',
|
||||
});
|
||||
seedStatuses(database, 'playlist-1', 'completed');
|
||||
const evaluator = evaluatorFor(database, 2_001);
|
||||
|
||||
const snapshot = await readXtreamCatalogDatabaseSnapshot(evaluator, {
|
||||
captureStoppedEpochMs: 2_000,
|
||||
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
||||
playlistId: 'playlist-1',
|
||||
requireFromPath: '/workspace/dist/apps/electron-backend/main.js',
|
||||
});
|
||||
|
||||
assert.deepEqual(snapshot, {
|
||||
captureStoppedEpochMs: 2_000,
|
||||
categories: {
|
||||
live: categoryPartition(1, 91_101),
|
||||
movies: categoryPartition(1, 92_101),
|
||||
series: categoryPartition(1, 93_101),
|
||||
},
|
||||
categoryCount: 3,
|
||||
content: {
|
||||
live: contentPartition(1_000, 1_000_000),
|
||||
movie: contentPartition(1_000, 2_000_000),
|
||||
series: contentPartition(1_000, 3_000_000),
|
||||
},
|
||||
contentCount: 3_000,
|
||||
databaseReadStartedEpochMs: 2_001,
|
||||
importStatuses: {
|
||||
live: 'completed',
|
||||
movie: 'completed',
|
||||
series: 'completed',
|
||||
},
|
||||
matchingPlaylistCount: 1,
|
||||
matchingPlaylistType: 'xtream',
|
||||
playlistCount: 1,
|
||||
});
|
||||
database.close();
|
||||
});
|
||||
|
||||
it('reports malformed fixture rows and incomplete category cardinality', async () => {
|
||||
const database = createDatabase();
|
||||
seedPlaylist(database, 'playlist-1');
|
||||
database.exec(`
|
||||
INSERT INTO categories (playlist_id, name, type, xtream_id)
|
||||
VALUES ('playlist-1', 'wrong category', 'live', 91101);
|
||||
INSERT INTO content (category_id, title, xtream_id, type)
|
||||
VALUES (last_insert_rowid(), 'wrong title', 1000000, 'live');
|
||||
`);
|
||||
seedStatuses(database, 'playlist-1', 'cancelled');
|
||||
|
||||
const snapshot = await readXtreamCatalogDatabaseSnapshot(
|
||||
evaluatorFor(database, 3_001),
|
||||
{
|
||||
captureStoppedEpochMs: 3_000,
|
||||
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
||||
playlistId: 'playlist-1',
|
||||
requireFromPath:
|
||||
'/workspace/dist/apps/electron-backend/main.js',
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(snapshot.categories.live.invalidNameCount, 1);
|
||||
assert.equal(snapshot.content.live.invalidTitleCount, 1);
|
||||
assert.equal(snapshot.content.live.invalidCategoryCardinalityCount, 1);
|
||||
assert.deepEqual(snapshot.importStatuses, {
|
||||
live: 'cancelled',
|
||||
movie: 'cancelled',
|
||||
series: 'cancelled',
|
||||
});
|
||||
database.close();
|
||||
});
|
||||
|
||||
it('returns an empty deleted state without inventing a playlist type', async () => {
|
||||
const database = createDatabase();
|
||||
seedStatuses(database, 'playlist-1', 'completed');
|
||||
|
||||
const snapshot = await readXtreamCatalogDatabaseSnapshot(
|
||||
evaluatorFor(database, 4_001),
|
||||
{
|
||||
captureStoppedEpochMs: 4_000,
|
||||
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
||||
playlistId: 'playlist-1',
|
||||
requireFromPath:
|
||||
'/workspace/dist/apps/electron-backend/main.js',
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(snapshot.categoryCount, 0);
|
||||
assert.equal(snapshot.contentCount, 0);
|
||||
assert.equal(snapshot.matchingPlaylistCount, 0);
|
||||
assert.equal(snapshot.matchingPlaylistType, null);
|
||||
assert.equal(snapshot.playlistCount, 0);
|
||||
database.close();
|
||||
});
|
||||
|
||||
it('fails closed for pre-capture reads and malformed main-process results', async () => {
|
||||
const database = createDatabase();
|
||||
const base = {
|
||||
captureStoppedEpochMs: 5_000,
|
||||
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
||||
playlistId: 'playlist-1',
|
||||
requireFromPath: '/workspace/dist/apps/electron-backend/main.js',
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
readXtreamCatalogDatabaseSnapshot(
|
||||
evaluatorFor(database, 4_999),
|
||||
base
|
||||
),
|
||||
/invalid Xtream catalog database read/
|
||||
);
|
||||
await assert.rejects(
|
||||
readXtreamCatalogDatabaseSnapshot(
|
||||
{
|
||||
evaluate: async () => ({
|
||||
databaseReadStartedEpochMs: 5_001,
|
||||
rows: [],
|
||||
}),
|
||||
},
|
||||
base
|
||||
),
|
||||
/invalid Xtream catalog database read/
|
||||
);
|
||||
await assert.rejects(
|
||||
readXtreamCatalogDatabaseSnapshot(evaluatorFor(database, 5_001), {
|
||||
...base,
|
||||
dataDirectory: 'relative',
|
||||
}),
|
||||
/invalid Xtream catalog database read/
|
||||
);
|
||||
database.close();
|
||||
});
|
||||
|
||||
it('accepts high-resolution fractional capture and database epochs', async () => {
|
||||
const database = createDatabase();
|
||||
|
||||
const snapshot = await readXtreamCatalogDatabaseSnapshot(
|
||||
evaluatorFor(database, 6_000.75),
|
||||
{
|
||||
captureStoppedEpochMs: 6_000.5,
|
||||
dataDirectory: '/tmp/iptvnator-performance-fixture',
|
||||
playlistId: 'playlist-1',
|
||||
requireFromPath:
|
||||
'/workspace/dist/apps/electron-backend/main.js',
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(snapshot.captureStoppedEpochMs, 6_000.5);
|
||||
assert.equal(snapshot.databaseReadStartedEpochMs, 6_000.75);
|
||||
database.close();
|
||||
});
|
||||
|
||||
it('uses the high-resolution epoch clock inside the main-process read boundary', () => {
|
||||
const source = readFileSync(
|
||||
resolve(
|
||||
process.cwd(),
|
||||
'src/performance/xtream-catalog-database-reader.ts'
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(source, /getBuiltinModule\(\s*'node:perf_hooks'\s*\)/u);
|
||||
assert.match(
|
||||
source,
|
||||
/perfHooks\.performance\.timeOrigin\s*\+\s*perfHooks\.performance\.now\(\)/u
|
||||
);
|
||||
assert.doesNotMatch(source, /Date\.now\(\)/u);
|
||||
});
|
||||
});
|
||||
|
||||
interface QueryDescriptor {
|
||||
readonly parameters: readonly (number | string)[];
|
||||
readonly sql: string;
|
||||
}
|
||||
|
||||
interface EvaluationInput {
|
||||
readonly queries: readonly QueryDescriptor[];
|
||||
}
|
||||
|
||||
function evaluatorFor(
|
||||
database: TestDatabase,
|
||||
databaseReadStartedEpochMs: number
|
||||
): XtreamCatalogDatabaseEvaluator {
|
||||
return {
|
||||
evaluate: async (_pageFunction: unknown, value: unknown) => {
|
||||
const input = value as unknown as EvaluationInput;
|
||||
return {
|
||||
databaseReadStartedEpochMs,
|
||||
rows: input.queries.map((query) =>
|
||||
database.prepare(query.sql).get(...query.parameters)
|
||||
),
|
||||
};
|
||||
},
|
||||
} as unknown as XtreamCatalogDatabaseEvaluator;
|
||||
}
|
||||
|
||||
function createDatabase(): TestDatabase {
|
||||
const database = new DatabaseSync(':memory:');
|
||||
database.exec(`
|
||||
CREATE TABLE playlists (id TEXT PRIMARY KEY, type TEXT NOT NULL);
|
||||
CREATE TABLE app_state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
playlist_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
xtream_id INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE content (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
xtream_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
return database;
|
||||
}
|
||||
|
||||
function seedPlaylist(database: TestDatabase, playlistId: string): void {
|
||||
database
|
||||
.prepare('INSERT INTO playlists (id, type) VALUES (?, ?)')
|
||||
.run(playlistId, 'xtream');
|
||||
}
|
||||
|
||||
function seedStatuses(
|
||||
database: TestDatabase,
|
||||
playlistId: string,
|
||||
status: string
|
||||
): void {
|
||||
const insert = database.prepare(
|
||||
'INSERT INTO app_state (key, value) VALUES (?, ?)'
|
||||
);
|
||||
for (const type of ['live', 'movie', 'series']) {
|
||||
insert.run(`xtream-import-status:${playlistId}:${type}`, status);
|
||||
}
|
||||
}
|
||||
|
||||
function seedPartition(
|
||||
database: TestDatabase,
|
||||
options: {
|
||||
readonly categoryId: number;
|
||||
readonly categoryName: string;
|
||||
readonly categoryType: string;
|
||||
readonly contentBase: number;
|
||||
readonly contentType: string;
|
||||
readonly playlistId: string;
|
||||
readonly titleLabel: string;
|
||||
}
|
||||
): void {
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO categories (playlist_id, name, type, xtream_id)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
options.playlistId,
|
||||
options.categoryName,
|
||||
options.categoryType,
|
||||
options.categoryId
|
||||
);
|
||||
const categoryRow = database
|
||||
.prepare('SELECT id FROM categories WHERE playlist_id = ? AND type = ?')
|
||||
.get(options.playlistId, options.categoryType) as { id: number };
|
||||
const insert = database.prepare(
|
||||
`INSERT INTO content (category_id, title, xtream_id, type)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
);
|
||||
for (let index = 0; index < 1_000; index += 1) {
|
||||
insert.run(
|
||||
categoryRow.id,
|
||||
`Performance ${options.titleLabel} ${String(index + 1).padStart(
|
||||
6,
|
||||
'0'
|
||||
)}`,
|
||||
options.contentBase + index,
|
||||
options.contentType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function categoryPartition(count: number, id: number) {
|
||||
return {
|
||||
count,
|
||||
distinctXtreamIdCount: count,
|
||||
invalidNameCount: 0,
|
||||
maxXtreamId: id,
|
||||
minXtreamId: id,
|
||||
};
|
||||
}
|
||||
|
||||
function contentPartition(count: number, id: number) {
|
||||
return {
|
||||
count,
|
||||
distinctXtreamIdCount: count,
|
||||
invalidCategoryCardinalityCount: 0,
|
||||
invalidTitleCount: 0,
|
||||
maxXtreamId: id + count - 1,
|
||||
minXtreamId: id,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import type { ElectronApplication } from '@playwright/test';
|
||||
import { isAbsolute, join } from 'node:path';
|
||||
|
||||
import type { XtreamCatalogDatabaseSnapshot } from './xtream-catalog-database-verification';
|
||||
import {
|
||||
buildXtreamCatalogDatabaseQueries,
|
||||
type XtreamCatalogDatabaseQuery,
|
||||
} from './xtream-catalog-database-queries';
|
||||
|
||||
export type XtreamCatalogDatabaseEvaluator = Pick<
|
||||
ElectronApplication,
|
||||
'evaluate'
|
||||
>;
|
||||
|
||||
export interface XtreamCatalogDatabaseReadOptions {
|
||||
readonly captureStoppedEpochMs: number;
|
||||
readonly dataDirectory: string;
|
||||
readonly playlistId: string;
|
||||
readonly requireFromPath: string;
|
||||
}
|
||||
|
||||
interface DatabaseReadInput {
|
||||
readonly databasePath: string;
|
||||
readonly queries: readonly XtreamCatalogDatabaseQuery[];
|
||||
readonly requireFromPath: string;
|
||||
}
|
||||
|
||||
interface DatabaseReadTransport {
|
||||
readonly databaseReadStartedEpochMs: number;
|
||||
readonly rows: readonly unknown[];
|
||||
}
|
||||
|
||||
export async function readXtreamCatalogDatabaseSnapshot(
|
||||
evaluator: XtreamCatalogDatabaseEvaluator,
|
||||
options: XtreamCatalogDatabaseReadOptions
|
||||
): Promise<XtreamCatalogDatabaseSnapshot> {
|
||||
try {
|
||||
assertOptions(options);
|
||||
const queries = buildXtreamCatalogDatabaseQueries(options.playlistId);
|
||||
const transport = (await evaluator.evaluate(
|
||||
async (_electron, input: DatabaseReadInput) => {
|
||||
interface SqliteStatement {
|
||||
get(...parameters: readonly (number | string)[]): unknown;
|
||||
}
|
||||
interface SqliteDatabase {
|
||||
close(): void;
|
||||
pragma(source: string): unknown;
|
||||
prepare(source: string): SqliteStatement;
|
||||
}
|
||||
interface SqliteConstructor {
|
||||
new (
|
||||
path: string,
|
||||
options: {
|
||||
readonly: true;
|
||||
fileMustExist: true;
|
||||
}
|
||||
): SqliteDatabase;
|
||||
}
|
||||
interface NodeModuleBuiltin {
|
||||
createRequire(path: string): (id: string) => unknown;
|
||||
}
|
||||
interface NodePerfHooksBuiltin {
|
||||
readonly performance: {
|
||||
now(): number;
|
||||
readonly timeOrigin: number;
|
||||
};
|
||||
}
|
||||
|
||||
const perfHooks = process.getBuiltinModule(
|
||||
'node:perf_hooks'
|
||||
) as NodePerfHooksBuiltin;
|
||||
const databaseReadStartedEpochMs =
|
||||
perfHooks.performance.timeOrigin +
|
||||
perfHooks.performance.now();
|
||||
const nodeModule = process.getBuiltinModule(
|
||||
'node:module'
|
||||
) as NodeModuleBuiltin;
|
||||
const requireFromBuild = nodeModule.createRequire(
|
||||
input.requireFromPath
|
||||
);
|
||||
const Sqlite = requireFromBuild(
|
||||
'better-sqlite3'
|
||||
) as SqliteConstructor;
|
||||
const database = new Sqlite(input.databasePath, {
|
||||
readonly: true,
|
||||
fileMustExist: true,
|
||||
});
|
||||
try {
|
||||
database.pragma('query_only = ON');
|
||||
return {
|
||||
databaseReadStartedEpochMs,
|
||||
rows: input.queries.map((query) =>
|
||||
database.prepare(query.sql).get(...query.parameters)
|
||||
),
|
||||
};
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
databasePath: join(
|
||||
options.dataDirectory,
|
||||
'databases',
|
||||
'iptvnator.db'
|
||||
),
|
||||
queries,
|
||||
requireFromPath: options.requireFromPath,
|
||||
}
|
||||
)) as DatabaseReadTransport;
|
||||
return snapshotTransport(
|
||||
transport,
|
||||
options.captureStoppedEpochMs,
|
||||
queries.length
|
||||
);
|
||||
} catch {
|
||||
throw new Error('invalid Xtream catalog database read');
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotTransport(
|
||||
value: unknown,
|
||||
captureStoppedEpochMs: number,
|
||||
expectedRowCount: number
|
||||
): XtreamCatalogDatabaseSnapshot {
|
||||
const transport = exactRecord(value, [
|
||||
'databaseReadStartedEpochMs',
|
||||
'rows',
|
||||
]);
|
||||
const databaseReadStartedEpochMs = transport[
|
||||
'databaseReadStartedEpochMs'
|
||||
] as unknown;
|
||||
const rows = transport['rows'];
|
||||
if (
|
||||
!isPositiveFinite(databaseReadStartedEpochMs) ||
|
||||
databaseReadStartedEpochMs < captureStoppedEpochMs ||
|
||||
!Array.isArray(rows) ||
|
||||
rows.length !== expectedRowCount
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
|
||||
const playlist = numericRecord(rows[0], [
|
||||
'playlistCount',
|
||||
'matchingPlaylistCount',
|
||||
'matchingPlaylistType',
|
||||
]);
|
||||
const categoryCount = singleCount(rows[1], 'categoryCount');
|
||||
const contentCount = singleCount(rows[2], 'contentCount');
|
||||
const categories = [rows[3], rows[4], rows[5]].map(categoryPartition);
|
||||
const content = [rows[6], rows[7], rows[8]].map(contentPartition);
|
||||
const importStatuses = statusRecord(rows[9]);
|
||||
const matchingPlaylistType = playlist['matchingPlaylistType'];
|
||||
if (
|
||||
matchingPlaylistType !== null &&
|
||||
typeof matchingPlaylistType !== 'string'
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
captureStoppedEpochMs,
|
||||
categories: Object.freeze({
|
||||
live: categories[0],
|
||||
movies: categories[1],
|
||||
series: categories[2],
|
||||
}),
|
||||
categoryCount,
|
||||
content: Object.freeze({
|
||||
live: content[0],
|
||||
movie: content[1],
|
||||
series: content[2],
|
||||
}),
|
||||
contentCount,
|
||||
databaseReadStartedEpochMs,
|
||||
importStatuses,
|
||||
matchingPlaylistCount: nonNegativeInteger(
|
||||
playlist['matchingPlaylistCount']
|
||||
),
|
||||
matchingPlaylistType,
|
||||
playlistCount: nonNegativeInteger(playlist['playlistCount']),
|
||||
});
|
||||
}
|
||||
|
||||
function categoryPartition(value: unknown) {
|
||||
const row = numericRecord(value, [
|
||||
'count',
|
||||
'distinctXtreamIdCount',
|
||||
'invalidNameCount',
|
||||
'maxXtreamId',
|
||||
'minXtreamId',
|
||||
]);
|
||||
return Object.freeze({
|
||||
count: nonNegativeInteger(row['count']),
|
||||
distinctXtreamIdCount: nonNegativeInteger(row['distinctXtreamIdCount']),
|
||||
invalidNameCount: nonNegativeInteger(row['invalidNameCount']),
|
||||
maxXtreamId: nullableInteger(row['maxXtreamId']),
|
||||
minXtreamId: nullableInteger(row['minXtreamId']),
|
||||
});
|
||||
}
|
||||
|
||||
function contentPartition(value: unknown) {
|
||||
const row = numericRecord(value, [
|
||||
'count',
|
||||
'distinctXtreamIdCount',
|
||||
'invalidCategoryCardinalityCount',
|
||||
'invalidTitleCount',
|
||||
'maxXtreamId',
|
||||
'minXtreamId',
|
||||
]);
|
||||
return Object.freeze({
|
||||
count: nonNegativeInteger(row['count']),
|
||||
distinctXtreamIdCount: nonNegativeInteger(row['distinctXtreamIdCount']),
|
||||
invalidCategoryCardinalityCount: nonNegativeInteger(
|
||||
row['invalidCategoryCardinalityCount']
|
||||
),
|
||||
invalidTitleCount: nonNegativeInteger(row['invalidTitleCount']),
|
||||
maxXtreamId: nullableInteger(row['maxXtreamId']),
|
||||
minXtreamId: nullableInteger(row['minXtreamId']),
|
||||
});
|
||||
}
|
||||
|
||||
function singleCount(value: unknown, key: string): number {
|
||||
return nonNegativeInteger(numericRecord(value, [key])[key]);
|
||||
}
|
||||
|
||||
function statusRecord(value: unknown) {
|
||||
const row = exactRecord(value, ['live', 'movie', 'series']);
|
||||
for (const key of ['live', 'movie', 'series']) {
|
||||
if (row[key] !== null && typeof row[key] !== 'string') {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
live: row['live'] as string | null,
|
||||
movie: row['movie'] as string | null,
|
||||
series: row['series'] as string | null,
|
||||
});
|
||||
}
|
||||
|
||||
function numericRecord(
|
||||
value: unknown,
|
||||
keys: readonly string[]
|
||||
): Record<string, unknown> {
|
||||
return exactRecord(value, keys);
|
||||
}
|
||||
|
||||
function exactRecord(
|
||||
value: unknown,
|
||||
expectedKeys: readonly string[]
|
||||
): Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
invalid();
|
||||
}
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expectedKeys.length ||
|
||||
keys.some(
|
||||
(key) => typeof key !== 'string' || !expectedKeys.includes(key)
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.fromEntries(
|
||||
expectedKeys.map((key) => [key, Reflect.get(value, key)])
|
||||
);
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: unknown): number {
|
||||
if (
|
||||
typeof value !== 'number' ||
|
||||
!Number.isSafeInteger(value) ||
|
||||
value < 0
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function nullableInteger(value: unknown): number | null {
|
||||
if (value === null) return null;
|
||||
return nonNegativeInteger(value);
|
||||
}
|
||||
|
||||
function assertOptions(options: XtreamCatalogDatabaseReadOptions): void {
|
||||
if (
|
||||
!isPositiveFinite(options.captureStoppedEpochMs) ||
|
||||
!isAbsolute(options.dataDirectory) ||
|
||||
!isAbsolute(options.requireFromPath) ||
|
||||
options.playlistId.length === 0 ||
|
||||
options.playlistId.length > 256
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function isPositiveFinite(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamPerformanceManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
assertXtreamCatalogDatabaseSnapshot,
|
||||
createXtreamCatalogVerification,
|
||||
type XtreamCatalogDatabaseSnapshot,
|
||||
} from './xtream-catalog-database-verification';
|
||||
|
||||
describe('Xtream catalog database verification', () => {
|
||||
it('accepts the exact synthetic catalog after import and refresh scenarios', () => {
|
||||
for (const scenarioId of [
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
XTREAM_SCENARIO_ID.REFRESH_LARGE,
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI,
|
||||
] as const) {
|
||||
const snapshot = importedSnapshot();
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamCatalogDatabaseSnapshot(
|
||||
snapshot,
|
||||
manifest(),
|
||||
scenarioId
|
||||
)
|
||||
);
|
||||
assert.deepEqual(
|
||||
createXtreamCatalogVerification(
|
||||
snapshot,
|
||||
manifest(),
|
||||
scenarioId
|
||||
),
|
||||
{
|
||||
databaseStateValid: true,
|
||||
fixtureIdentityValid: true,
|
||||
providerCategoryCount: 100,
|
||||
providerItemCount: 100_000,
|
||||
verifiedAfterCapture: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('pins the current cancellation cleanup state and complete source deletion', () => {
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamCatalogDatabaseSnapshot(
|
||||
emptySnapshot({
|
||||
matchingPlaylistCount: 1,
|
||||
matchingPlaylistType: 'xtream',
|
||||
playlistCount: 1,
|
||||
statuses: {
|
||||
live: 'failed',
|
||||
movie: 'failed',
|
||||
series: 'failed',
|
||||
},
|
||||
}),
|
||||
manifest(),
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
)
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamCatalogDatabaseSnapshot(
|
||||
emptySnapshot({
|
||||
matchingPlaylistCount: 1,
|
||||
matchingPlaylistType: 'xtream',
|
||||
playlistCount: 1,
|
||||
statuses: {
|
||||
live: 'cancelled',
|
||||
movie: 'cancelled',
|
||||
series: 'cancelled',
|
||||
},
|
||||
}),
|
||||
manifest(),
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
),
|
||||
/invalid Xtream database catalog state/
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamCatalogDatabaseSnapshot(
|
||||
emptySnapshot(),
|
||||
manifest(),
|
||||
XTREAM_SCENARIO_ID.DELETE_LARGE
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects partial, foreign, mis-associated, and non-terminal database state', () => {
|
||||
const baseline = importedSnapshot();
|
||||
const invalid: XtreamCatalogDatabaseSnapshot[] = [
|
||||
{ ...baseline, playlistCount: 2 },
|
||||
{
|
||||
...baseline,
|
||||
categories: {
|
||||
...baseline.categories,
|
||||
live: {
|
||||
...baseline.categories.live,
|
||||
distinctXtreamIdCount: 59,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseline,
|
||||
content: {
|
||||
...baseline.content,
|
||||
movie: {
|
||||
...baseline.content.movie,
|
||||
maxXtreamId: 2_019_998,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseline,
|
||||
content: {
|
||||
...baseline.content,
|
||||
series: {
|
||||
...baseline.content.series,
|
||||
invalidCategoryCardinalityCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseline,
|
||||
content: {
|
||||
...baseline.content,
|
||||
live: {
|
||||
...baseline.content.live,
|
||||
invalidTitleCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseline,
|
||||
importStatuses: {
|
||||
...baseline.importStatuses,
|
||||
movie: 'importing',
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseline,
|
||||
databaseReadStartedEpochMs: baseline.captureStoppedEpochMs - 1,
|
||||
},
|
||||
];
|
||||
|
||||
for (const snapshot of invalid) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamCatalogDatabaseSnapshot(
|
||||
snapshot,
|
||||
manifest(),
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
),
|
||||
/^Error: invalid Xtream database catalog state$/u
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects cancellation residue and a delete that left the source row behind', () => {
|
||||
const cancelled = emptySnapshot({
|
||||
matchingPlaylistCount: 1,
|
||||
matchingPlaylistType: 'xtream',
|
||||
playlistCount: 1,
|
||||
statuses: {
|
||||
live: 'failed',
|
||||
movie: 'failed',
|
||||
series: 'failed',
|
||||
},
|
||||
});
|
||||
const cancelWithResidue: XtreamCatalogDatabaseSnapshot = {
|
||||
...cancelled,
|
||||
content: {
|
||||
...cancelled.content,
|
||||
live: {
|
||||
...cancelled.content.live,
|
||||
count: 1,
|
||||
distinctXtreamIdCount: 1,
|
||||
maxXtreamId: 1_000_000,
|
||||
minXtreamId: 1_000_000,
|
||||
},
|
||||
},
|
||||
contentCount: 1,
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamCatalogDatabaseSnapshot(
|
||||
cancelWithResidue,
|
||||
manifest(),
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
),
|
||||
/invalid Xtream database catalog state/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamCatalogDatabaseSnapshot(
|
||||
emptySnapshot({
|
||||
matchingPlaylistCount: 1,
|
||||
matchingPlaylistType: 'xtream',
|
||||
playlistCount: 1,
|
||||
}),
|
||||
manifest(),
|
||||
XTREAM_SCENARIO_ID.DELETE_LARGE
|
||||
),
|
||||
/invalid Xtream database catalog state/
|
||||
);
|
||||
});
|
||||
|
||||
it('uses one canonical fixture manifest snapshot', () => {
|
||||
const expectedManifest = manifest();
|
||||
const reducedCounts = {
|
||||
categories: { live: 1, series: 1, total: 3, vod: 1 },
|
||||
items: { live: 1, series: 1, total: 3, vod: 1 },
|
||||
};
|
||||
let countReads = 0;
|
||||
const changingManifest = {
|
||||
...expectedManifest,
|
||||
get counts() {
|
||||
countReads += 1;
|
||||
return countReads === 1
|
||||
? expectedManifest.counts
|
||||
: reducedCounts;
|
||||
},
|
||||
};
|
||||
const reducedSnapshot: XtreamCatalogDatabaseSnapshot = {
|
||||
...importedSnapshot(),
|
||||
categories: {
|
||||
live: partition(1, 91_101),
|
||||
movies: partition(1, 92_101),
|
||||
series: partition(1, 93_101),
|
||||
},
|
||||
categoryCount: 3,
|
||||
content: {
|
||||
live: contentPartition(1, 1_000_000),
|
||||
movie: contentPartition(1, 2_000_000),
|
||||
series: contentPartition(1, 3_000_000),
|
||||
},
|
||||
contentCount: 3,
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamCatalogVerification(
|
||||
reducedSnapshot,
|
||||
changingManifest,
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
),
|
||||
/invalid Xtream database catalog state/
|
||||
);
|
||||
assert.equal(countReads, 1);
|
||||
});
|
||||
});
|
||||
|
||||
function manifest(): XtreamPerformanceManifest {
|
||||
return {
|
||||
bytes: 12_345_678,
|
||||
catalogSha256: 'a'.repeat(64),
|
||||
counts: {
|
||||
categories: { live: 60, series: 20, total: 100, vod: 20 },
|
||||
items: {
|
||||
live: 60_000,
|
||||
series: 20_000,
|
||||
total: 100_000,
|
||||
vod: 20_000,
|
||||
},
|
||||
},
|
||||
epoch: 1,
|
||||
scenario: 'performance-100k',
|
||||
seed: 91_001,
|
||||
};
|
||||
}
|
||||
|
||||
function importedSnapshot(): XtreamCatalogDatabaseSnapshot {
|
||||
return {
|
||||
captureStoppedEpochMs: 1_000,
|
||||
categories: {
|
||||
live: partition(60, 91_101),
|
||||
movies: partition(20, 92_101),
|
||||
series: partition(20, 93_101),
|
||||
},
|
||||
categoryCount: 100,
|
||||
content: {
|
||||
live: contentPartition(60_000, 1_000_000),
|
||||
movie: contentPartition(20_000, 2_000_000),
|
||||
series: contentPartition(20_000, 3_000_000),
|
||||
},
|
||||
contentCount: 100_000,
|
||||
databaseReadStartedEpochMs: 1_001,
|
||||
importStatuses: {
|
||||
live: 'completed',
|
||||
movie: 'completed',
|
||||
series: 'completed',
|
||||
},
|
||||
matchingPlaylistCount: 1,
|
||||
matchingPlaylistType: 'xtream',
|
||||
playlistCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function emptySnapshot(
|
||||
options: {
|
||||
matchingPlaylistCount?: number;
|
||||
matchingPlaylistType?: string | null;
|
||||
playlistCount?: number;
|
||||
statuses?: XtreamCatalogDatabaseSnapshot['importStatuses'];
|
||||
} = {}
|
||||
): XtreamCatalogDatabaseSnapshot {
|
||||
return {
|
||||
captureStoppedEpochMs: 1_000,
|
||||
categories: {
|
||||
live: partition(0, null),
|
||||
movies: partition(0, null),
|
||||
series: partition(0, null),
|
||||
},
|
||||
categoryCount: 0,
|
||||
content: {
|
||||
live: contentPartition(0, null),
|
||||
movie: contentPartition(0, null),
|
||||
series: contentPartition(0, null),
|
||||
},
|
||||
contentCount: 0,
|
||||
databaseReadStartedEpochMs: 1_001,
|
||||
importStatuses: options.statuses ?? {
|
||||
live: 'completed',
|
||||
movie: 'completed',
|
||||
series: 'completed',
|
||||
},
|
||||
matchingPlaylistCount: options.matchingPlaylistCount ?? 0,
|
||||
matchingPlaylistType: options.matchingPlaylistType ?? null,
|
||||
playlistCount: options.playlistCount ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function partition(
|
||||
count: number,
|
||||
minXtreamId: number | null
|
||||
): XtreamCatalogDatabaseSnapshot['categories']['live'] {
|
||||
return {
|
||||
count,
|
||||
distinctXtreamIdCount: count,
|
||||
invalidNameCount: 0,
|
||||
maxXtreamId: minXtreamId === null ? null : minXtreamId + count - 1,
|
||||
minXtreamId,
|
||||
};
|
||||
}
|
||||
|
||||
function contentPartition(
|
||||
count: number,
|
||||
minXtreamId: number | null
|
||||
): XtreamCatalogDatabaseSnapshot['content']['live'] {
|
||||
return {
|
||||
count,
|
||||
distinctXtreamIdCount: count,
|
||||
invalidCategoryCardinalityCount: 0,
|
||||
invalidTitleCount: 0,
|
||||
maxXtreamId: minXtreamId === null ? null : minXtreamId + count - 1,
|
||||
minXtreamId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamPerformanceManifest,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { parseXtreamManifest } from './xtream-control-state-schema';
|
||||
|
||||
interface XtreamCatalogPartition {
|
||||
readonly count: number;
|
||||
readonly distinctXtreamIdCount: number;
|
||||
readonly invalidNameCount: number;
|
||||
readonly maxXtreamId: number | null;
|
||||
readonly minXtreamId: number | null;
|
||||
}
|
||||
|
||||
interface XtreamContentPartition {
|
||||
readonly count: number;
|
||||
readonly distinctXtreamIdCount: number;
|
||||
readonly invalidCategoryCardinalityCount: number;
|
||||
readonly invalidTitleCount: number;
|
||||
readonly maxXtreamId: number | null;
|
||||
readonly minXtreamId: number | null;
|
||||
}
|
||||
|
||||
export interface XtreamCatalogDatabaseSnapshot {
|
||||
readonly captureStoppedEpochMs: number;
|
||||
readonly categories: {
|
||||
readonly live: XtreamCatalogPartition;
|
||||
readonly movies: XtreamCatalogPartition;
|
||||
readonly series: XtreamCatalogPartition;
|
||||
};
|
||||
readonly categoryCount: number;
|
||||
readonly content: {
|
||||
readonly live: XtreamContentPartition;
|
||||
readonly movie: XtreamContentPartition;
|
||||
readonly series: XtreamContentPartition;
|
||||
};
|
||||
readonly contentCount: number;
|
||||
readonly databaseReadStartedEpochMs: number;
|
||||
readonly importStatuses: {
|
||||
readonly live: string | null;
|
||||
readonly movie: string | null;
|
||||
readonly series: string | null;
|
||||
};
|
||||
readonly matchingPlaylistCount: number;
|
||||
readonly matchingPlaylistType: string | null;
|
||||
readonly playlistCount: number;
|
||||
}
|
||||
|
||||
export interface XtreamCatalogVerification {
|
||||
readonly databaseStateValid: true;
|
||||
readonly fixtureIdentityValid: true;
|
||||
readonly providerCategoryCount: number;
|
||||
readonly providerItemCount: number;
|
||||
readonly verifiedAfterCapture: true;
|
||||
}
|
||||
|
||||
export function assertXtreamCatalogDatabaseSnapshot(
|
||||
snapshot: unknown,
|
||||
manifest: XtreamPerformanceManifest,
|
||||
scenarioId: XtreamScenarioId
|
||||
): asserts snapshot is XtreamCatalogDatabaseSnapshot {
|
||||
try {
|
||||
const canonicalManifest = parseXtreamManifest(manifest);
|
||||
assertCatalogSnapshot(snapshot, canonicalManifest, scenarioId);
|
||||
} catch {
|
||||
throw new Error('invalid Xtream database catalog state');
|
||||
}
|
||||
}
|
||||
|
||||
export function createXtreamCatalogVerification(
|
||||
snapshot: unknown,
|
||||
manifest: XtreamPerformanceManifest,
|
||||
scenarioId: XtreamScenarioId
|
||||
): XtreamCatalogVerification {
|
||||
try {
|
||||
const canonicalManifest = parseXtreamManifest(manifest);
|
||||
assertCatalogSnapshot(snapshot, canonicalManifest, scenarioId);
|
||||
return Object.freeze({
|
||||
databaseStateValid: true,
|
||||
fixtureIdentityValid: true,
|
||||
providerCategoryCount: canonicalManifest.counts.categories.total,
|
||||
providerItemCount: canonicalManifest.counts.items.total,
|
||||
verifiedAfterCapture: true,
|
||||
});
|
||||
} catch {
|
||||
throw new Error('invalid Xtream database catalog state');
|
||||
}
|
||||
}
|
||||
|
||||
function assertCatalogSnapshot(
|
||||
snapshot: unknown,
|
||||
manifest: XtreamPerformanceManifest,
|
||||
scenarioId: XtreamScenarioId
|
||||
): void {
|
||||
const expected = expectedSnapshot(manifest, scenarioId);
|
||||
const candidate = snapshotTopLevel(snapshot);
|
||||
if (
|
||||
!isSnapshotTimingValid(candidate) ||
|
||||
!isDeepStrictEqual(withoutSnapshotTiming(candidate), expected)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function expectedSnapshot(
|
||||
manifest: XtreamPerformanceManifest,
|
||||
scenarioId: XtreamScenarioId
|
||||
): XtreamCatalogDatabaseSnapshot {
|
||||
if (!Object.values(XTREAM_SCENARIO_ID).includes(scenarioId)) {
|
||||
invalid();
|
||||
}
|
||||
const empty = isEmptyScenario(scenarioId);
|
||||
const deleted = scenarioId === XTREAM_SCENARIO_ID.DELETE_LARGE;
|
||||
const cancelled = scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT;
|
||||
const expected = {
|
||||
captureStoppedEpochMs: 1,
|
||||
categories: {
|
||||
live: categoryPartition(
|
||||
empty ? 0 : manifest.counts.categories.live,
|
||||
91_101
|
||||
),
|
||||
movies: categoryPartition(
|
||||
empty ? 0 : manifest.counts.categories.vod,
|
||||
92_101
|
||||
),
|
||||
series: categoryPartition(
|
||||
empty ? 0 : manifest.counts.categories.series,
|
||||
93_101
|
||||
),
|
||||
},
|
||||
categoryCount: empty ? 0 : manifest.counts.categories.total,
|
||||
content: {
|
||||
live: contentPartition(
|
||||
empty ? 0 : manifest.counts.items.live,
|
||||
1_000_000
|
||||
),
|
||||
movie: contentPartition(
|
||||
empty ? 0 : manifest.counts.items.vod,
|
||||
2_000_000
|
||||
),
|
||||
series: contentPartition(
|
||||
empty ? 0 : manifest.counts.items.series,
|
||||
3_000_000
|
||||
),
|
||||
},
|
||||
contentCount: empty ? 0 : manifest.counts.items.total,
|
||||
databaseReadStartedEpochMs: 1,
|
||||
importStatuses: {
|
||||
live: cancelled ? 'failed' : 'completed',
|
||||
movie: cancelled ? 'failed' : 'completed',
|
||||
series: cancelled ? 'failed' : 'completed',
|
||||
},
|
||||
matchingPlaylistCount: deleted ? 0 : 1,
|
||||
matchingPlaylistType: deleted ? null : 'xtream',
|
||||
playlistCount: deleted ? 0 : 1,
|
||||
} satisfies XtreamCatalogDatabaseSnapshot;
|
||||
return {
|
||||
...expected,
|
||||
captureStoppedEpochMs: Number.NaN,
|
||||
databaseReadStartedEpochMs: Number.NaN,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshotTopLevel(value: unknown): XtreamCatalogDatabaseSnapshot {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
invalid();
|
||||
}
|
||||
return { ...value } as XtreamCatalogDatabaseSnapshot;
|
||||
}
|
||||
|
||||
function isSnapshotTimingValid(
|
||||
snapshot: XtreamCatalogDatabaseSnapshot
|
||||
): boolean {
|
||||
if (
|
||||
!isPositiveFinite(snapshot.captureStoppedEpochMs) ||
|
||||
!isPositiveFinite(snapshot.databaseReadStartedEpochMs) ||
|
||||
snapshot.databaseReadStartedEpochMs < snapshot.captureStoppedEpochMs
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function withoutSnapshotTiming(
|
||||
snapshot: XtreamCatalogDatabaseSnapshot
|
||||
): XtreamCatalogDatabaseSnapshot {
|
||||
return {
|
||||
...snapshot,
|
||||
captureStoppedEpochMs: Number.NaN,
|
||||
databaseReadStartedEpochMs: Number.NaN,
|
||||
};
|
||||
}
|
||||
|
||||
function isEmptyScenario(scenarioId: XtreamScenarioId): boolean {
|
||||
return (
|
||||
scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT ||
|
||||
scenarioId === XTREAM_SCENARIO_ID.DELETE_LARGE
|
||||
);
|
||||
}
|
||||
|
||||
function categoryPartition(
|
||||
count: number,
|
||||
firstXtreamId: number
|
||||
): XtreamCatalogPartition {
|
||||
return {
|
||||
count,
|
||||
distinctXtreamIdCount: count,
|
||||
invalidNameCount: 0,
|
||||
maxXtreamId: count === 0 ? null : firstXtreamId + count - 1,
|
||||
minXtreamId: count === 0 ? null : firstXtreamId,
|
||||
};
|
||||
}
|
||||
|
||||
function contentPartition(
|
||||
count: number,
|
||||
firstXtreamId: number
|
||||
): XtreamContentPartition {
|
||||
return {
|
||||
count,
|
||||
distinctXtreamIdCount: count,
|
||||
invalidCategoryCardinalityCount: 0,
|
||||
invalidTitleCount: 0,
|
||||
maxXtreamId: count === 0 ? null : firstXtreamId + count - 1,
|
||||
minXtreamId: count === 0 ? null : firstXtreamId,
|
||||
};
|
||||
}
|
||||
|
||||
function isPositiveFinite(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import type {
|
||||
XtreamControlState,
|
||||
XtreamPerformanceManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { createXtreamControlClient } from './xtream-control-client';
|
||||
|
||||
const TOKEN = 'never-persist-this-control-token';
|
||||
const MANIFEST: XtreamPerformanceManifest = {
|
||||
epoch: 1,
|
||||
scenario: 'performance-100k',
|
||||
seed: 91_001,
|
||||
counts: {
|
||||
categories: { live: 60, vod: 20, series: 20, total: 100 },
|
||||
items: {
|
||||
live: 60_000,
|
||||
vod: 20_000,
|
||||
series: 20_000,
|
||||
total: 100_000,
|
||||
},
|
||||
},
|
||||
bytes: 100,
|
||||
catalogSha256:
|
||||
'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
};
|
||||
const STATE: XtreamControlState = {
|
||||
epoch: 1,
|
||||
prepared: MANIFEST,
|
||||
rules: [],
|
||||
heldIds: [],
|
||||
heldCount: 0,
|
||||
occurrences: [],
|
||||
ledger: [],
|
||||
};
|
||||
|
||||
describe('Xtream performance control client', () => {
|
||||
it('exposes only fixed prepare, reset, and state operations', async () => {
|
||||
const calls: RecordedCall[] = [];
|
||||
const client = createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token: TOKEN,
|
||||
fetchImpl: fakeFetch(calls),
|
||||
});
|
||||
|
||||
assert.deepEqual(Object.keys(client).sort(), [
|
||||
'prepare',
|
||||
'resetAll',
|
||||
'resetObservations',
|
||||
'state',
|
||||
]);
|
||||
assert.deepEqual(await client.prepare(), MANIFEST);
|
||||
await client.resetObservations();
|
||||
await client.resetAll();
|
||||
assert.deepEqual(await client.state(), STATE);
|
||||
|
||||
assert.deepEqual(
|
||||
calls.map(({ body, method, path }) => ({ body, method, path })),
|
||||
[
|
||||
{
|
||||
body: { scenario: 'performance-100k' },
|
||||
method: 'POST',
|
||||
path: '/__control/prepare',
|
||||
},
|
||||
{
|
||||
body: { mode: 'observations' },
|
||||
method: 'POST',
|
||||
path: '/__control/reset',
|
||||
},
|
||||
{
|
||||
body: { mode: 'all' },
|
||||
method: 'POST',
|
||||
path: '/__control/reset',
|
||||
},
|
||||
{
|
||||
body: null,
|
||||
method: 'GET',
|
||||
path: '/__control/state',
|
||||
},
|
||||
]
|
||||
);
|
||||
assert.ok(
|
||||
calls.every(
|
||||
({ headers, redirect }) =>
|
||||
redirect === 'error' &&
|
||||
headers.get('x-iptvnator-performance-token') === TOKEN
|
||||
)
|
||||
);
|
||||
assert.equal(
|
||||
calls.at(-1)?.headers.get('content-type'),
|
||||
null,
|
||||
'GET must not carry a synthetic JSON body'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the token closure-only and redacts transport failures', async () => {
|
||||
const client = createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token: TOKEN,
|
||||
fetchImpl: async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: TOKEN,
|
||||
password: 'performance',
|
||||
}),
|
||||
{ status: 500 }
|
||||
),
|
||||
});
|
||||
|
||||
assert.doesNotMatch(JSON.stringify(client), new RegExp(TOKEN));
|
||||
await assert.rejects(
|
||||
() => client.prepare(),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
/prepare failed with HTTP 500/.test(error.message) &&
|
||||
!error.message.includes(TOKEN) &&
|
||||
!error.message.includes('performance')
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects redirects and a response finalized at any second origin', async () => {
|
||||
let forwardedToken = false;
|
||||
const client = createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token: TOKEN,
|
||||
fetchImpl: async (_input, init) => {
|
||||
if (init?.redirect !== 'error') {
|
||||
forwardedToken = new Headers(init?.headers).has(
|
||||
'x-iptvnator-performance-token'
|
||||
);
|
||||
}
|
||||
return responseAt(
|
||||
MANIFEST,
|
||||
200,
|
||||
'http://127.0.0.1:43124/__control/prepare'
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => client.prepare(),
|
||||
/redirect|origin|transport failed/
|
||||
);
|
||||
assert.equal(forwardedToken, false);
|
||||
});
|
||||
|
||||
it('validates control responses without echoing their unsafe payloads', async () => {
|
||||
const client = createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token: TOKEN,
|
||||
fetchImpl: async () =>
|
||||
responseAt(
|
||||
{
|
||||
username: 'performance',
|
||||
secret: TOKEN,
|
||||
},
|
||||
200,
|
||||
'http://127.0.0.1:43123/__control/state'
|
||||
),
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => client.state(),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
/invalid control response/.test(error.message) &&
|
||||
!error.message.includes(TOKEN) &&
|
||||
!error.message.includes('performance')
|
||||
);
|
||||
});
|
||||
|
||||
it('sanitizes adversarial response metadata accessors', async () => {
|
||||
for (const property of ['ok', 'status', 'redirected', 'url'] as const) {
|
||||
const client = createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token: TOKEN,
|
||||
fetchImpl: async (input) => {
|
||||
const response = {
|
||||
json: async () => MANIFEST,
|
||||
ok: property === 'status' ? false : true,
|
||||
redirected: false,
|
||||
status: 200,
|
||||
url: String(input),
|
||||
};
|
||||
Object.defineProperty(response, property, {
|
||||
get() {
|
||||
throw new Error(TOKEN);
|
||||
},
|
||||
});
|
||||
return response as Response;
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => client.prepare(),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
/invalid response metadata/.test(error.message) &&
|
||||
!error.message.includes(TOKEN)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('sanitizes reset response Proxy failures', async () => {
|
||||
const resetBody = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys() {
|
||||
throw new Error(TOKEN);
|
||||
},
|
||||
}
|
||||
);
|
||||
const client = createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token: TOKEN,
|
||||
fetchImpl: async (input) =>
|
||||
({
|
||||
json: async () => resetBody,
|
||||
ok: true,
|
||||
redirected: false,
|
||||
status: 200,
|
||||
url: String(input),
|
||||
}) as Response,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => client.resetAll(),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
error.message === 'Xtream all reset invalid control response' &&
|
||||
!error.message.includes(TOKEN)
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects non-loopback origins and empty tokens before issuing I/O', () => {
|
||||
let calls = 0;
|
||||
const fetchImpl: typeof fetch = async () => {
|
||||
calls += 1;
|
||||
return new Response();
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamControlClient({
|
||||
origin: 'https://provider.example',
|
||||
token: TOKEN,
|
||||
fetchImpl,
|
||||
}),
|
||||
/exact Xtream HTTP loopback origin/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token: '',
|
||||
fetchImpl,
|
||||
}),
|
||||
/non-empty control token/
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
it('snapshots the validated origin and token before mutable getters can change them', async () => {
|
||||
const calls: RecordedCall[] = [];
|
||||
let currentOrigin = 'http://127.0.0.1:43123';
|
||||
let currentToken = TOKEN;
|
||||
let originReads = 0;
|
||||
let tokenReads = 0;
|
||||
const client = createXtreamControlClient({
|
||||
fetchImpl: fakeFetch(calls),
|
||||
get origin() {
|
||||
originReads += 1;
|
||||
return currentOrigin;
|
||||
},
|
||||
get token() {
|
||||
tokenReads += 1;
|
||||
return currentToken;
|
||||
},
|
||||
});
|
||||
|
||||
currentOrigin = 'https://provider.example';
|
||||
currentToken = 'attacker-controlled-token';
|
||||
await client.prepare();
|
||||
|
||||
assert.equal(originReads, 1);
|
||||
assert.equal(tokenReads, 1);
|
||||
assert.equal(calls[0]?.path, '/__control/prepare');
|
||||
assert.equal(
|
||||
calls[0]?.headers.get('x-iptvnator-performance-token'),
|
||||
TOKEN
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid runtime token values before issuing I/O', () => {
|
||||
let calls = 0;
|
||||
const fetchImpl: typeof fetch = async () => {
|
||||
calls += 1;
|
||||
return new Response();
|
||||
};
|
||||
|
||||
for (const token of [
|
||||
null,
|
||||
undefined,
|
||||
42,
|
||||
{},
|
||||
' ',
|
||||
'line\r\nbreak',
|
||||
'x'.repeat(257),
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
createXtreamControlClient({
|
||||
origin: 'http://127.0.0.1:43123',
|
||||
token,
|
||||
fetchImpl,
|
||||
} as unknown as Parameters<
|
||||
typeof createXtreamControlClient
|
||||
>[0]),
|
||||
/non-empty control token/
|
||||
);
|
||||
}
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
});
|
||||
|
||||
interface RecordedCall {
|
||||
readonly body: Record<string, unknown> | null;
|
||||
readonly headers: Headers;
|
||||
readonly method: string;
|
||||
readonly path: string;
|
||||
readonly redirect: RequestRedirect | undefined;
|
||||
}
|
||||
|
||||
function fakeFetch(calls: RecordedCall[]): typeof fetch {
|
||||
return async (input, init) => {
|
||||
const url = new URL(
|
||||
typeof input === 'string'
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.href
|
||||
: input.url
|
||||
);
|
||||
const method = init?.method ?? 'GET';
|
||||
const body =
|
||||
typeof init?.body === 'string'
|
||||
? (JSON.parse(init.body) as Record<string, unknown>)
|
||||
: null;
|
||||
calls.push({
|
||||
body,
|
||||
headers: new Headers(init?.headers),
|
||||
method,
|
||||
path: url.pathname,
|
||||
redirect: init?.redirect,
|
||||
});
|
||||
if (url.pathname.endsWith('/prepare')) {
|
||||
return responseAt(MANIFEST, 200, url.href);
|
||||
}
|
||||
if (url.pathname.endsWith('/reset')) {
|
||||
return responseAt(
|
||||
{
|
||||
epoch: MANIFEST.epoch,
|
||||
mode: body?.['mode'],
|
||||
},
|
||||
200,
|
||||
url.href
|
||||
);
|
||||
}
|
||||
return responseAt(STATE, 200, url.href);
|
||||
};
|
||||
}
|
||||
|
||||
function responseAt(value: unknown, status: number, url: string): Response {
|
||||
const response = Response.json(value, { status });
|
||||
Object.defineProperty(response, 'url', { value: url });
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import type {
|
||||
XtreamControlState,
|
||||
XtreamPerformanceManifest,
|
||||
XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
assertXtreamArtifactRedacted,
|
||||
assertXtreamManifest,
|
||||
assertXtreamManifestIdentity,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import {
|
||||
parseXtreamControlState,
|
||||
parseXtreamManifest,
|
||||
} from './xtream-control-state-schema';
|
||||
import { assertXtreamScenarioObservations } from './xtream-control-observation-contract';
|
||||
|
||||
export {
|
||||
assertXtreamArtifactRedacted,
|
||||
assertXtreamManifest,
|
||||
assertXtreamManifestIdentity,
|
||||
parseXtreamControlState,
|
||||
parseXtreamManifest,
|
||||
};
|
||||
|
||||
export interface XtreamControlClientOptions {
|
||||
readonly fetchImpl?: typeof fetch;
|
||||
readonly origin: string;
|
||||
readonly token: string;
|
||||
}
|
||||
|
||||
export interface XtreamControlClient {
|
||||
prepare(): Promise<XtreamPerformanceManifest>;
|
||||
resetAll(): Promise<void>;
|
||||
resetObservations(): Promise<void>;
|
||||
state(): Promise<XtreamControlState>;
|
||||
}
|
||||
|
||||
export function createXtreamControlClient(
|
||||
options: XtreamControlClientOptions
|
||||
): XtreamControlClient {
|
||||
let origin: string;
|
||||
let token: string;
|
||||
let configuredFetch: typeof fetch | undefined;
|
||||
try {
|
||||
origin = options.origin;
|
||||
token = options.token;
|
||||
configuredFetch = options.fetchImpl;
|
||||
} catch {
|
||||
throw new Error('Xtream benchmark control options are invalid');
|
||||
}
|
||||
assertXtreamMockOrigin(origin);
|
||||
if (!isValidControlToken(token)) {
|
||||
throw new Error('Xtream benchmark requires a non-empty control token');
|
||||
}
|
||||
const fetchImpl = configuredFetch ?? globalThis.fetch;
|
||||
const request = async (
|
||||
path: '/__control/prepare' | '/__control/reset' | '/__control/state',
|
||||
label: string,
|
||||
body?: Readonly<Record<string, string>>
|
||||
): Promise<unknown> => {
|
||||
let response: Response;
|
||||
const requestUrl = `${origin}${path}`;
|
||||
try {
|
||||
response = await fetchImpl(requestUrl, {
|
||||
method: body ? 'POST' : 'GET',
|
||||
redirect: 'error',
|
||||
headers: {
|
||||
'x-iptvnator-performance-token': token,
|
||||
...(body ? { 'content-type': 'application/json' } : {}),
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
} catch {
|
||||
throw new Error(`Xtream control ${label} transport failed`);
|
||||
}
|
||||
const metadata = snapshotControlResponse(response, label);
|
||||
if (!metadata.ok) {
|
||||
throw new Error(
|
||||
`Xtream control ${label} failed with HTTP ${metadata.status}`
|
||||
);
|
||||
}
|
||||
if (metadata.redirected || metadata.url !== requestUrl) {
|
||||
throw new Error(
|
||||
`Xtream control ${label} redirect or origin rejected`
|
||||
);
|
||||
}
|
||||
try {
|
||||
return (await Reflect.apply(
|
||||
metadata.json,
|
||||
response,
|
||||
[]
|
||||
)) as unknown;
|
||||
} catch {
|
||||
throw new Error(`Xtream ${label} invalid control response`);
|
||||
}
|
||||
};
|
||||
const reset = async (mode: 'all' | 'observations'): Promise<void> => {
|
||||
const value = await request('/__control/reset', `${mode} reset`, {
|
||||
mode,
|
||||
});
|
||||
try {
|
||||
if (!isRecordWithKeys(value, ['epoch', 'mode'])) {
|
||||
throw new Error('invalid reset response');
|
||||
}
|
||||
const epoch = Reflect.get(value, 'epoch');
|
||||
const responseMode = Reflect.get(value, 'mode');
|
||||
if (!isPositiveSafeInteger(epoch) || responseMode !== mode) {
|
||||
throw new Error('invalid reset response');
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Xtream ${mode} reset invalid control response`);
|
||||
}
|
||||
};
|
||||
return Object.freeze({
|
||||
async prepare() {
|
||||
const value = await request('/__control/prepare', 'prepare', {
|
||||
scenario: 'performance-100k',
|
||||
});
|
||||
try {
|
||||
return parseXtreamManifest(value);
|
||||
} catch {
|
||||
throw new Error('Xtream prepare invalid control response');
|
||||
}
|
||||
},
|
||||
resetAll: () => reset('all'),
|
||||
resetObservations: () => reset('observations'),
|
||||
async state() {
|
||||
const value = await request('/__control/state', 'state');
|
||||
try {
|
||||
return parseXtreamControlState(value);
|
||||
} catch {
|
||||
throw new Error('Xtream state invalid control response');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function assertXtreamMockOrigin(value: unknown): void {
|
||||
if (typeof value !== 'string') throw invalidOrigin();
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw invalidOrigin();
|
||||
}
|
||||
if (
|
||||
url.protocol !== 'http:' ||
|
||||
url.hostname !== '127.0.0.1' ||
|
||||
url.port.length === 0 ||
|
||||
url.username.length > 0 ||
|
||||
url.password.length > 0 ||
|
||||
url.pathname !== '/' ||
|
||||
url.search.length > 0 ||
|
||||
url.hash.length > 0 ||
|
||||
value !== url.origin
|
||||
) {
|
||||
throw invalidOrigin();
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotControlResponse(
|
||||
response: Response,
|
||||
label: string
|
||||
): {
|
||||
readonly json: () => Promise<unknown>;
|
||||
readonly ok: boolean;
|
||||
readonly redirected: boolean;
|
||||
readonly status: number;
|
||||
readonly url: string;
|
||||
} {
|
||||
try {
|
||||
const json = Reflect.get(response, 'json') as unknown;
|
||||
const ok = Reflect.get(response, 'ok') as unknown;
|
||||
const redirected = Reflect.get(response, 'redirected') as unknown;
|
||||
const status = Reflect.get(response, 'status') as unknown;
|
||||
const url = Reflect.get(response, 'url') as unknown;
|
||||
if (
|
||||
typeof json !== 'function' ||
|
||||
typeof ok !== 'boolean' ||
|
||||
typeof redirected !== 'boolean' ||
|
||||
!Number.isSafeInteger(status) ||
|
||||
Number(status) < 100 ||
|
||||
Number(status) > 599 ||
|
||||
typeof url !== 'string'
|
||||
) {
|
||||
throw new Error('invalid response metadata');
|
||||
}
|
||||
return {
|
||||
json: json as () => Promise<unknown>,
|
||||
ok,
|
||||
redirected,
|
||||
status: Number(status),
|
||||
url,
|
||||
};
|
||||
} catch {
|
||||
throw new Error(`Xtream control ${label} invalid response metadata`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertXtreamControlReadyAfterObservationReset(
|
||||
input: unknown,
|
||||
expectedManifest: XtreamPerformanceManifest
|
||||
): asserts input is XtreamControlState {
|
||||
try {
|
||||
const state = parseXtreamControlState(input);
|
||||
assertPreparedState(state, expectedManifest);
|
||||
if (state.occurrences.length !== 0 || state.ledger.length !== 0) {
|
||||
throw new Error('observations remain');
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
'invalid Xtream mock readiness after observation reset'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertXtreamControlStateForScenario(
|
||||
scenarioId: XtreamScenarioId,
|
||||
input: unknown,
|
||||
expectedManifest: XtreamPerformanceManifest
|
||||
): asserts input is XtreamControlState {
|
||||
try {
|
||||
const state = parseXtreamControlState(input);
|
||||
assertPreparedState(state, expectedManifest);
|
||||
assertXtreamScenarioObservations(scenarioId, state);
|
||||
} catch {
|
||||
throw new Error('invalid Xtream mock observations');
|
||||
}
|
||||
}
|
||||
|
||||
function assertPreparedState(
|
||||
state: XtreamControlState,
|
||||
manifest: XtreamPerformanceManifest
|
||||
): void {
|
||||
if (
|
||||
state.prepared === null ||
|
||||
state.epoch !== manifest.epoch ||
|
||||
state.prepared.epoch !== manifest.epoch ||
|
||||
state.rules.length !== 0 ||
|
||||
state.heldCount !== 0 ||
|
||||
state.heldIds.length !== 0
|
||||
) {
|
||||
throw new Error('state residue');
|
||||
}
|
||||
assertXtreamManifestIdentity(manifest, state.prepared);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isRecordWithKeys(
|
||||
value: unknown,
|
||||
keys: readonly string[]
|
||||
): value is Record<string, unknown> {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
Object.keys(value).length === keys.length &&
|
||||
keys.every((key) => Object.prototype.hasOwnProperty.call(value, key))
|
||||
);
|
||||
}
|
||||
|
||||
function isPositiveSafeInteger(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0;
|
||||
}
|
||||
|
||||
function isValidControlToken(value: unknown): value is string {
|
||||
return typeof value === 'string' && /^[\x21-\x7e]{1,256}$/u.test(value);
|
||||
}
|
||||
|
||||
function invalidOrigin(): Error {
|
||||
return new Error('mock must use the exact Xtream HTTP loopback origin');
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamControlState,
|
||||
type XtreamPerformanceManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { assertXtreamControlStateForScenario } from './xtream-control-client';
|
||||
|
||||
const MANIFEST: XtreamPerformanceManifest = {
|
||||
epoch: 1,
|
||||
scenario: 'performance-100k',
|
||||
seed: 91_001,
|
||||
counts: {
|
||||
categories: { live: 60, vod: 20, series: 20, total: 100 },
|
||||
items: {
|
||||
live: 60_000,
|
||||
vod: 20_000,
|
||||
series: 20_000,
|
||||
total: 100_000,
|
||||
},
|
||||
},
|
||||
bytes: 12_345_678,
|
||||
catalogSha256:
|
||||
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
||||
};
|
||||
|
||||
const FULL_ACTIONS = [
|
||||
'get_account_info',
|
||||
'get_live_categories',
|
||||
'get_vod_categories',
|
||||
'get_series_categories',
|
||||
'get_live_streams',
|
||||
'get_vod_streams',
|
||||
'get_series',
|
||||
] as const;
|
||||
|
||||
describe('Xtream mock observation contract', () => {
|
||||
it('models exact account-info multiplicity for every measured window', () => {
|
||||
assert.doesNotThrow(() =>
|
||||
validate(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
controlState(FULL_ACTIONS, 2)
|
||||
)
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
validate(
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT,
|
||||
controlState(FULL_ACTIONS.slice(0, 5), 2)
|
||||
)
|
||||
);
|
||||
for (const scenarioId of [
|
||||
XTREAM_SCENARIO_ID.REFRESH_LARGE,
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI,
|
||||
] as const) {
|
||||
assert.doesNotThrow(() =>
|
||||
validate(scenarioId, controlState(FULL_ACTIONS, 2, true))
|
||||
);
|
||||
}
|
||||
assert.doesNotThrow(() =>
|
||||
validate(XTREAM_SCENARIO_ID.DELETE_LARGE, controlState([], 0))
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects account-info multiplicity from a different scenario window', () => {
|
||||
for (const [scenarioId, state] of [
|
||||
[
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
controlState(FULL_ACTIONS, 1),
|
||||
],
|
||||
[
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT,
|
||||
controlState(FULL_ACTIONS.slice(0, 5), 1),
|
||||
],
|
||||
[XTREAM_SCENARIO_ID.REFRESH_LARGE, controlState(FULL_ACTIONS, 1)],
|
||||
[XTREAM_SCENARIO_ID.BACKGROUND_UI, controlState(FULL_ACTIONS, 1)],
|
||||
] as const) {
|
||||
assert.throws(
|
||||
() => validate(scenarioId, state),
|
||||
/invalid Xtream mock observations/
|
||||
);
|
||||
}
|
||||
|
||||
for (const scenarioId of [
|
||||
XTREAM_SCENARIO_ID.REFRESH_LARGE,
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI,
|
||||
] as const) {
|
||||
assert.throws(
|
||||
() => validate(scenarioId, controlState(FULL_ACTIONS, 2)),
|
||||
/invalid Xtream mock observations/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows account overlap but requires complete route-account causality', () => {
|
||||
const exact = controlState(FULL_ACTIONS, 2);
|
||||
const duplicateArrival = {
|
||||
...exact,
|
||||
ledger: exact.ledger.map((entry) =>
|
||||
entry.action === 'get_account_info' &&
|
||||
entry.occurrence === 2 &&
|
||||
entry.status === 'responded'
|
||||
? { ...entry, status: 'arrived' }
|
||||
: entry
|
||||
),
|
||||
};
|
||||
const overlappingAccounts = {
|
||||
...exact,
|
||||
ledger: exact.ledger.map((entry) =>
|
||||
entry.action === 'get_account_info' &&
|
||||
entry.occurrence === 1 &&
|
||||
entry.status === 'responded'
|
||||
? { ...entry, atMs: 25 }
|
||||
: entry
|
||||
),
|
||||
};
|
||||
const routeAccountAfterCategories = {
|
||||
...exact,
|
||||
ledger: exact.ledger.map((entry) =>
|
||||
entry.action === 'get_account_info' &&
|
||||
entry.occurrence === 2 &&
|
||||
entry.status === 'responded'
|
||||
? { ...entry, atMs: 35 }
|
||||
: entry
|
||||
),
|
||||
};
|
||||
const reversedAccountStarts = {
|
||||
...exact,
|
||||
ledger: exact.ledger.map((entry) =>
|
||||
entry.action === 'get_account_info' &&
|
||||
entry.occurrence === 2 &&
|
||||
entry.status === 'arrived'
|
||||
? { ...entry, atMs: 9 }
|
||||
: entry
|
||||
),
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
validate(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
overlappingAccounts
|
||||
)
|
||||
);
|
||||
for (const invalid of [
|
||||
duplicateArrival,
|
||||
reversedAccountStarts,
|
||||
routeAccountAfterCategories,
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
validate(XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE, invalid),
|
||||
/invalid Xtream mock observations/
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function validate(
|
||||
scenarioId: Parameters<typeof assertXtreamControlStateForScenario>[0],
|
||||
state: XtreamControlState
|
||||
): void {
|
||||
assertXtreamControlStateForScenario(scenarioId, state, MANIFEST);
|
||||
}
|
||||
|
||||
function controlState(
|
||||
actions: readonly string[],
|
||||
accountCount: number,
|
||||
routeAccountAfterContent = false
|
||||
): XtreamControlState {
|
||||
const counts = new Map(
|
||||
actions.map((action) => [
|
||||
action,
|
||||
action === 'get_account_info' ? accountCount : 1,
|
||||
])
|
||||
);
|
||||
let order = 1;
|
||||
return {
|
||||
epoch: MANIFEST.epoch,
|
||||
prepared: MANIFEST,
|
||||
rules: [],
|
||||
heldIds: [],
|
||||
heldCount: 0,
|
||||
occurrences: [...counts].map(([action, count]) => ({
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct',
|
||||
action,
|
||||
categoryId: 'all',
|
||||
count,
|
||||
})),
|
||||
ledger: [...counts].flatMap(([action, count]) =>
|
||||
Array.from({ length: count }, (_, index) => {
|
||||
const routeAccount =
|
||||
routeAccountAfterContent &&
|
||||
action === 'get_account_info' &&
|
||||
index === 1;
|
||||
const arrived = routeAccount ? 1_000 : order++ * 10;
|
||||
return [
|
||||
{
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct',
|
||||
action,
|
||||
categoryId: 'all',
|
||||
epoch: MANIFEST.epoch,
|
||||
occurrence: index + 1,
|
||||
status: 'arrived',
|
||||
atMs: arrived,
|
||||
},
|
||||
{
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct',
|
||||
action,
|
||||
categoryId: 'all',
|
||||
epoch: MANIFEST.epoch,
|
||||
occurrence: index + 1,
|
||||
status: 'responded',
|
||||
atMs: arrived + 1,
|
||||
},
|
||||
];
|
||||
}).flat()
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamControlState,
|
||||
type XtreamLedgerEntry,
|
||||
type XtreamOccurrence,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
|
||||
const FULL_ACTIONS = [
|
||||
'get_account_info',
|
||||
'get_live_categories',
|
||||
'get_vod_categories',
|
||||
'get_series_categories',
|
||||
'get_live_streams',
|
||||
'get_vod_streams',
|
||||
'get_series',
|
||||
] as const;
|
||||
const CATEGORY_ACTIONS = FULL_ACTIONS.slice(1, 4);
|
||||
|
||||
interface ExpectedAction {
|
||||
readonly action: string;
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
interface RequestLifecycle {
|
||||
readonly arrived: number;
|
||||
readonly responded: number;
|
||||
}
|
||||
|
||||
export function assertXtreamScenarioObservations(
|
||||
scenarioId: XtreamScenarioId,
|
||||
state: XtreamControlState
|
||||
): void {
|
||||
const expected = expectedActions(scenarioId);
|
||||
if (
|
||||
state.occurrences.length !== expected.length ||
|
||||
state.ledger.length !==
|
||||
expected.reduce((total, entry) => total + entry.count * 2, 0)
|
||||
) {
|
||||
throw new Error('action cardinality');
|
||||
}
|
||||
|
||||
const lifecycle = new Map<string, readonly RequestLifecycle[]>();
|
||||
for (const expectedAction of expected) {
|
||||
const occurrences = state.occurrences.filter(
|
||||
(entry) => entry.action === expectedAction.action
|
||||
);
|
||||
const entries = state.ledger.filter(
|
||||
(entry) => entry.action === expectedAction.action
|
||||
);
|
||||
if (
|
||||
occurrences.length !== 1 ||
|
||||
!isExpectedOccurrence(occurrences[0], expectedAction.count) ||
|
||||
entries.length !== expectedAction.count * 2
|
||||
) {
|
||||
throw new Error('action occurrence');
|
||||
}
|
||||
|
||||
const requests = Array.from(
|
||||
{ length: expectedAction.count },
|
||||
(_, index) => requestLifecycle(entries, state.epoch, index + 1)
|
||||
);
|
||||
assertOccurrenceStartOrder(requests);
|
||||
lifecycle.set(expectedAction.action, requests);
|
||||
}
|
||||
if (
|
||||
state.occurrences.some(
|
||||
(entry) => !expected.some(({ action }) => action === entry.action)
|
||||
) ||
|
||||
state.ledger.some(
|
||||
(entry) => !expected.some(({ action }) => action === entry.action)
|
||||
)
|
||||
) {
|
||||
throw new Error('unexpected action');
|
||||
}
|
||||
assertActionOrder(scenarioId, expected, lifecycle);
|
||||
}
|
||||
|
||||
function expectedActions(
|
||||
scenarioId: XtreamScenarioId
|
||||
): readonly ExpectedAction[] {
|
||||
if (scenarioId === XTREAM_SCENARIO_ID.DELETE_LARGE) return [];
|
||||
const actions =
|
||||
scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
? FULL_ACTIONS.slice(0, 5)
|
||||
: FULL_ACTIONS;
|
||||
return actions.map((action) => ({
|
||||
action,
|
||||
count: action === 'get_account_info' ? 2 : 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function requestLifecycle(
|
||||
entries: readonly XtreamLedgerEntry[],
|
||||
epoch: number,
|
||||
occurrence: number
|
||||
): RequestLifecycle {
|
||||
const requestEntries = entries.filter(
|
||||
(entry) => entry.occurrence === occurrence
|
||||
);
|
||||
const arrived = requestEntries.find((entry) => entry.status === 'arrived');
|
||||
const responded = requestEntries.find(
|
||||
(entry) => entry.status === 'responded'
|
||||
);
|
||||
if (
|
||||
requestEntries.length !== 2 ||
|
||||
!isExpectedLedger(arrived, epoch, occurrence) ||
|
||||
!isExpectedLedger(responded, epoch, occurrence) ||
|
||||
arrived.status !== 'arrived' ||
|
||||
responded.status !== 'responded' ||
|
||||
arrived.atMs > responded.atMs
|
||||
) {
|
||||
throw new Error('action lifecycle');
|
||||
}
|
||||
return { arrived: arrived.atMs, responded: responded.atMs };
|
||||
}
|
||||
|
||||
function assertOccurrenceStartOrder(
|
||||
requests: readonly RequestLifecycle[]
|
||||
): void {
|
||||
for (let index = 1; index < requests.length; index += 1) {
|
||||
if (
|
||||
(requests[index - 1]?.arrived ?? Number.POSITIVE_INFINITY) >
|
||||
(requests[index]?.arrived ?? Number.NEGATIVE_INFINITY)
|
||||
) {
|
||||
throw new Error('occurrence start order');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertActionOrder(
|
||||
scenarioId: XtreamScenarioId,
|
||||
expected: readonly ExpectedAction[],
|
||||
lifecycle: ReadonlyMap<string, readonly RequestLifecycle[]>
|
||||
): void {
|
||||
if (expected.length === 0) return;
|
||||
const accounts = lifecycle.get('get_account_info');
|
||||
const routeHydratesAfterImport =
|
||||
scenarioId === XTREAM_SCENARIO_ID.REFRESH_LARGE ||
|
||||
scenarioId === XTREAM_SCENARIO_ID.BACKGROUND_UI;
|
||||
const account = routeHydratesAfterImport ? accounts?.[0] : accounts?.at(-1);
|
||||
const liveCategory = lifecycle.get(CATEGORY_ACTIONS[0])?.[0];
|
||||
const vodCategory = lifecycle.get(CATEGORY_ACTIONS[1])?.[0];
|
||||
const seriesCategory = lifecycle.get(CATEGORY_ACTIONS[2])?.[0];
|
||||
const live = lifecycle.get('get_live_streams')?.[0];
|
||||
if (
|
||||
!account ||
|
||||
!liveCategory ||
|
||||
!vodCategory ||
|
||||
!seriesCategory ||
|
||||
account.responded >
|
||||
Math.min(
|
||||
liveCategory.arrived,
|
||||
vodCategory.arrived,
|
||||
seriesCategory.arrived
|
||||
) ||
|
||||
!live ||
|
||||
Math.max(
|
||||
liveCategory.responded,
|
||||
vodCategory.responded,
|
||||
seriesCategory.responded
|
||||
) > live.arrived
|
||||
) {
|
||||
throw new Error('action order');
|
||||
}
|
||||
const vod = lifecycle.get('get_vod_streams')?.[0];
|
||||
const series = lifecycle.get('get_series')?.[0];
|
||||
if (
|
||||
(vod && live.responded > vod.arrived) ||
|
||||
(series && (!vod || vod.responded > series.arrived)) ||
|
||||
(routeHydratesAfterImport &&
|
||||
(!series ||
|
||||
!accounts?.[1] ||
|
||||
series.responded > accounts[1].arrived))
|
||||
) {
|
||||
throw new Error('content order');
|
||||
}
|
||||
}
|
||||
|
||||
function isExpectedOccurrence(
|
||||
value: XtreamOccurrence | undefined,
|
||||
count: number
|
||||
): boolean {
|
||||
return (
|
||||
value?.scenario === 'performance-100k' &&
|
||||
value.transport === 'direct' &&
|
||||
value.categoryId === 'all' &&
|
||||
value.count === count
|
||||
);
|
||||
}
|
||||
|
||||
function isExpectedLedger(
|
||||
value: XtreamLedgerEntry | undefined,
|
||||
epoch: number,
|
||||
occurrence: number
|
||||
): value is XtreamLedgerEntry {
|
||||
return (
|
||||
value?.scenario === 'performance-100k' &&
|
||||
value.transport === 'direct' &&
|
||||
value.categoryId === 'all' &&
|
||||
value.epoch === epoch &&
|
||||
value.occurrence === occurrence
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import type {
|
||||
XtreamControlState,
|
||||
XtreamPerformanceManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { parseXtreamControlState } from './xtream-control-state-schema';
|
||||
|
||||
const MANIFEST: XtreamPerformanceManifest = {
|
||||
epoch: 1,
|
||||
scenario: 'performance-100k',
|
||||
seed: 91_001,
|
||||
counts: {
|
||||
categories: { live: 60, vod: 20, series: 20, total: 100 },
|
||||
items: {
|
||||
live: 60_000,
|
||||
vod: 20_000,
|
||||
series: 20_000,
|
||||
total: 100_000,
|
||||
},
|
||||
},
|
||||
bytes: 100,
|
||||
catalogSha256:
|
||||
'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
};
|
||||
const OCCURRENCE = {
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct',
|
||||
action: 'get_account_info',
|
||||
categoryId: 'all',
|
||||
count: 1,
|
||||
};
|
||||
const LEDGER = {
|
||||
scenario: 'performance-100k',
|
||||
transport: 'direct',
|
||||
action: 'get_account_info',
|
||||
categoryId: 'all',
|
||||
epoch: 1,
|
||||
occurrence: 1,
|
||||
status: 'arrived',
|
||||
atMs: 1,
|
||||
};
|
||||
|
||||
describe('Xtream control-state exact schema', () => {
|
||||
it('requires exact occurrence and ledger records with known lifecycle states', () => {
|
||||
const populated = state();
|
||||
assert.doesNotThrow(() => parseXtreamControlState(populated));
|
||||
|
||||
for (const invalid of [
|
||||
{
|
||||
...populated,
|
||||
occurrences: [{ ...OCCURRENCE, injected: true }],
|
||||
},
|
||||
{
|
||||
...populated,
|
||||
ledger: [{ ...LEDGER, injected: true }],
|
||||
},
|
||||
{
|
||||
...populated,
|
||||
ledger: [{ ...LEDGER, status: 'completed' }],
|
||||
},
|
||||
]) {
|
||||
assert.throws(
|
||||
() => parseXtreamControlState(invalid),
|
||||
/^Error: invalid control response$/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns a stable deep-frozen snapshot of nested accessor values', () => {
|
||||
let epoch = 1;
|
||||
let manifestBytes = MANIFEST.bytes;
|
||||
let occurrenceAction = OCCURRENCE.action;
|
||||
let ledgerStatus = LEDGER.status;
|
||||
const input = withAccessor(
|
||||
{
|
||||
...state(),
|
||||
prepared: withAccessor(
|
||||
{ ...MANIFEST },
|
||||
'bytes',
|
||||
() => manifestBytes
|
||||
),
|
||||
occurrences: [
|
||||
withAccessor(
|
||||
{ ...OCCURRENCE },
|
||||
'action',
|
||||
() => occurrenceAction
|
||||
),
|
||||
],
|
||||
ledger: [
|
||||
withAccessor({ ...LEDGER }, 'status', () => ledgerStatus),
|
||||
],
|
||||
},
|
||||
'epoch',
|
||||
() => epoch
|
||||
);
|
||||
|
||||
const parsed = parseXtreamControlState(input);
|
||||
epoch = 2;
|
||||
manifestBytes = 0;
|
||||
occurrenceAction = 'unknown';
|
||||
ledgerStatus = 'completed';
|
||||
|
||||
assert.equal(parsed.epoch, 1);
|
||||
assert.equal(parsed.prepared?.bytes, MANIFEST.bytes);
|
||||
assert.equal(parsed.occurrences[0]?.action, 'get_account_info');
|
||||
assert.equal(parsed.ledger[0]?.status, 'arrived');
|
||||
assert.ok(Object.isFrozen(parsed));
|
||||
assert.ok(Object.isFrozen(parsed.prepared));
|
||||
assert.ok(Object.isFrozen(parsed.prepared?.counts));
|
||||
assert.ok(Object.isFrozen(parsed.occurrences));
|
||||
assert.ok(Object.isFrozen(parsed.occurrences[0]));
|
||||
assert.ok(Object.isFrozen(parsed.ledger));
|
||||
assert.ok(Object.isFrozen(parsed.ledger[0]));
|
||||
});
|
||||
|
||||
it('sanitizes proxy and nested getter failures', () => {
|
||||
const secret = 'must-not-escape-control-state';
|
||||
const proxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys() {
|
||||
throw new Error(secret);
|
||||
},
|
||||
}
|
||||
);
|
||||
const nested = state();
|
||||
Object.defineProperty(nested.prepared, 'bytes', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
throw new Error(secret);
|
||||
},
|
||||
});
|
||||
|
||||
for (const invalid of [proxy, nested]) {
|
||||
assert.throws(
|
||||
() => parseXtreamControlState(invalid),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
error.message === 'invalid control response' &&
|
||||
!error.message.includes(secret)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function state(): XtreamControlState {
|
||||
return {
|
||||
epoch: 1,
|
||||
prepared: structuredClone(MANIFEST),
|
||||
rules: [],
|
||||
heldIds: [],
|
||||
heldCount: 0,
|
||||
occurrences: [{ ...OCCURRENCE }],
|
||||
ledger: [{ ...LEDGER }],
|
||||
};
|
||||
}
|
||||
|
||||
function withAccessor<T extends object, K extends keyof T>(
|
||||
value: T,
|
||||
key: K,
|
||||
get: () => T[K]
|
||||
): T {
|
||||
Object.defineProperty(value, key, { enumerable: true, get });
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import type {
|
||||
XtreamCatalogCounts,
|
||||
XtreamControlState,
|
||||
XtreamLedgerEntry,
|
||||
XtreamOccurrence,
|
||||
XtreamPerformanceManifest,
|
||||
} from './xtream-benchmark-contract';
|
||||
|
||||
const LIFECYCLE_STATUSES = [
|
||||
'aborted',
|
||||
'arrived',
|
||||
'blocked',
|
||||
'delayed',
|
||||
'responded',
|
||||
] as const;
|
||||
const STATE_KEYS = [
|
||||
'epoch',
|
||||
'prepared',
|
||||
'rules',
|
||||
'heldIds',
|
||||
'heldCount',
|
||||
'occurrences',
|
||||
'ledger',
|
||||
] as const;
|
||||
const MANIFEST_KEYS = [
|
||||
'epoch',
|
||||
'scenario',
|
||||
'seed',
|
||||
'counts',
|
||||
'bytes',
|
||||
'catalogSha256',
|
||||
] as const;
|
||||
const MAX_CONTROL_ARRAY_LENGTH = 4_096;
|
||||
|
||||
export function parseXtreamControlState(value: unknown): XtreamControlState {
|
||||
try {
|
||||
const input = snapshotExactRecord(value, STATE_KEYS);
|
||||
const heldIds = snapshotArray(input['heldIds']);
|
||||
const occurrences = snapshotArray(input['occurrences']).map(
|
||||
snapshotOccurrence
|
||||
);
|
||||
const ledger = snapshotArray(input['ledger']).map(snapshotLedger);
|
||||
const rules = snapshotArray(input['rules']).map((entry) =>
|
||||
snapshotJsonValue(entry, new WeakSet())
|
||||
);
|
||||
if (
|
||||
!isPositiveSafeInteger(input['epoch']) ||
|
||||
!heldIds.every((entry) => typeof entry === 'string') ||
|
||||
!isNonNegativeSafeInteger(input['heldCount']) ||
|
||||
input['heldCount'] !== heldIds.length
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const prepared =
|
||||
input['prepared'] === null
|
||||
? null
|
||||
: snapshotManifest(input['prepared']);
|
||||
return freezeDeep({
|
||||
epoch: input['epoch'],
|
||||
prepared,
|
||||
rules,
|
||||
heldIds: heldIds as string[],
|
||||
heldCount: input['heldCount'],
|
||||
occurrences,
|
||||
ledger,
|
||||
}) as XtreamControlState;
|
||||
} catch {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
export function parseXtreamManifest(value: unknown): XtreamPerformanceManifest {
|
||||
try {
|
||||
return freezeDeep(snapshotManifest(value));
|
||||
} catch {
|
||||
throw invalidManifest();
|
||||
}
|
||||
}
|
||||
|
||||
export function assertXtreamManifest(
|
||||
value: unknown
|
||||
): asserts value is XtreamPerformanceManifest {
|
||||
parseXtreamManifest(value);
|
||||
}
|
||||
|
||||
export function assertXtreamManifestIdentity(
|
||||
expected: XtreamPerformanceManifest,
|
||||
actual: unknown
|
||||
): asserts actual is XtreamPerformanceManifest {
|
||||
const expectedSnapshot = parseXtreamManifest(expected);
|
||||
const actualSnapshot = parseXtreamManifest(actual);
|
||||
const identity = (manifest: XtreamPerformanceManifest) => [
|
||||
manifest.scenario,
|
||||
manifest.seed,
|
||||
manifest.counts,
|
||||
manifest.bytes,
|
||||
manifest.catalogSha256,
|
||||
];
|
||||
if (
|
||||
JSON.stringify(identity(expectedSnapshot)) !==
|
||||
JSON.stringify(identity(actualSnapshot))
|
||||
) {
|
||||
throw new Error('Xtream performance fixture identity changed');
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotManifest(value: unknown): XtreamPerformanceManifest {
|
||||
const input = snapshotExactRecord(value, MANIFEST_KEYS);
|
||||
const counts = snapshotExactRecord(input['counts'], [
|
||||
'categories',
|
||||
'items',
|
||||
]);
|
||||
const categories = snapshotCounts(counts['categories'], [60, 20, 20, 100]);
|
||||
const items = snapshotCounts(
|
||||
counts['items'],
|
||||
[60_000, 20_000, 20_000, 100_000]
|
||||
);
|
||||
if (
|
||||
!isPositiveSafeInteger(input['epoch']) ||
|
||||
input['scenario'] !== 'performance-100k' ||
|
||||
input['seed'] !== 91_001 ||
|
||||
!isPositiveSafeInteger(input['bytes']) ||
|
||||
typeof input['catalogSha256'] !== 'string' ||
|
||||
!/^[a-f0-9]{64}$/u.test(input['catalogSha256'])
|
||||
) {
|
||||
throw invalidManifest();
|
||||
}
|
||||
return {
|
||||
epoch: input['epoch'],
|
||||
scenario: input['scenario'],
|
||||
seed: input['seed'],
|
||||
counts: { categories, items },
|
||||
bytes: input['bytes'],
|
||||
catalogSha256: input['catalogSha256'],
|
||||
};
|
||||
}
|
||||
|
||||
function snapshotCounts(
|
||||
value: unknown,
|
||||
expected: readonly number[]
|
||||
): XtreamCatalogCounts {
|
||||
const input = snapshotExactRecord(value, [
|
||||
'live',
|
||||
'vod',
|
||||
'series',
|
||||
'total',
|
||||
]);
|
||||
const actual = [
|
||||
input['live'],
|
||||
input['vod'],
|
||||
input['series'],
|
||||
input['total'],
|
||||
];
|
||||
if (actual.some((entry, index) => entry !== expected[index])) {
|
||||
throw invalidManifest();
|
||||
}
|
||||
return {
|
||||
live: input['live'] as number,
|
||||
vod: input['vod'] as number,
|
||||
series: input['series'] as number,
|
||||
total: input['total'] as number,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshotOccurrence(value: unknown): XtreamOccurrence {
|
||||
const input = snapshotExactRecord(value, [
|
||||
'scenario',
|
||||
'transport',
|
||||
'action',
|
||||
'categoryId',
|
||||
'count',
|
||||
]);
|
||||
if (
|
||||
!['scenario', 'transport', 'action', 'categoryId'].every(
|
||||
(key) => typeof input[key] === 'string'
|
||||
) ||
|
||||
!isPositiveSafeInteger(input['count'])
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return input as unknown as XtreamOccurrence;
|
||||
}
|
||||
|
||||
function snapshotLedger(value: unknown): XtreamLedgerEntry {
|
||||
const input = snapshotExactRecord(value, [
|
||||
'scenario',
|
||||
'transport',
|
||||
'action',
|
||||
'categoryId',
|
||||
'epoch',
|
||||
'occurrence',
|
||||
'status',
|
||||
'atMs',
|
||||
]);
|
||||
if (
|
||||
!['scenario', 'transport', 'action', 'categoryId'].every(
|
||||
(key) => typeof input[key] === 'string'
|
||||
) ||
|
||||
typeof input['status'] !== 'string' ||
|
||||
!LIFECYCLE_STATUSES.includes(input['status'] as never) ||
|
||||
!isPositiveSafeInteger(input['epoch']) ||
|
||||
!isPositiveSafeInteger(input['occurrence']) ||
|
||||
!isFiniteNonNegative(input['atMs'])
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return input as unknown as XtreamLedgerEntry;
|
||||
}
|
||||
|
||||
function snapshotExactRecord(
|
||||
value: unknown,
|
||||
expectedKeys: readonly string[]
|
||||
): Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
invalid();
|
||||
}
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expectedKeys.length ||
|
||||
keys.some(
|
||||
(key) => typeof key !== 'string' || !expectedKeys.includes(key)
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.fromEntries(
|
||||
expectedKeys.map((key) => [key, Reflect.get(value, key)])
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotArray(value: unknown): unknown[] {
|
||||
if (!Array.isArray(value)) invalid();
|
||||
const length = Reflect.get(value, 'length') as unknown;
|
||||
if (
|
||||
!isNonNegativeSafeInteger(length) ||
|
||||
length > MAX_CONTROL_ARRAY_LENGTH
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const expected = new Set([
|
||||
'length',
|
||||
...Array.from({ length }, (_, index) => String(index)),
|
||||
]);
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expected.size ||
|
||||
keys.some((key) => typeof key !== 'string' || !expected.has(key))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Array.from({ length }, (_, index) =>
|
||||
Reflect.get(value, String(index))
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotJsonValue(value: unknown, stack: WeakSet<object>): unknown {
|
||||
if (
|
||||
value === null ||
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'boolean' ||
|
||||
(typeof value === 'number' && Number.isFinite(value))
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value !== 'object') invalid();
|
||||
if (stack.has(value)) invalid();
|
||||
stack.add(value);
|
||||
try {
|
||||
if (Array.isArray(value)) {
|
||||
return snapshotArray(value).map((entry) =>
|
||||
snapshotJsonValue(entry, stack)
|
||||
);
|
||||
}
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (keys.some((key) => typeof key !== 'string')) invalid();
|
||||
const input = snapshotExactRecord(value, keys as string[]);
|
||||
return Object.fromEntries(
|
||||
(keys as string[]).map((key) => [
|
||||
key,
|
||||
snapshotJsonValue(input[key], stack),
|
||||
])
|
||||
);
|
||||
} finally {
|
||||
stack.delete(value);
|
||||
}
|
||||
}
|
||||
|
||||
function freezeDeep<T>(value: T): T {
|
||||
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
||||
Object.values(value).forEach(freezeDeep);
|
||||
Object.freeze(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isPositiveSafeInteger(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) > 0;
|
||||
}
|
||||
|
||||
function isNonNegativeSafeInteger(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && Number(value) >= 0;
|
||||
}
|
||||
|
||||
function isFiniteNonNegative(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid control response');
|
||||
}
|
||||
|
||||
function invalidManifest(): Error {
|
||||
return new Error('invalid Xtream performance manifest');
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
XTREAM_MAIN_PERFORMANCE_PHASE,
|
||||
XTREAM_PRELOAD_PERFORMANCE_METHOD,
|
||||
XtreamCodeActions,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
import {
|
||||
createXtreamIpcMarkerCapture,
|
||||
type XtreamIpcMarkerCaptureOptions,
|
||||
} from './xtream-ipc-marker-events';
|
||||
|
||||
const options: XtreamIpcMarkerCaptureOptions = {
|
||||
actions: Object.values(XtreamCodeActions),
|
||||
mainPhases: Object.values(XTREAM_MAIN_PERFORMANCE_PHASE),
|
||||
methods: Object.values(XTREAM_PRELOAD_PERFORMANCE_METHOD),
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
describe('Xtream playlist delete correlation', () => {
|
||||
it('correlates the follow-up playlist delete without fabricating an operation ID', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
|
||||
assert.equal(capture.acceptPreload(42, 100, deleteMarker()), true);
|
||||
assert.deepEqual(capture.matchDatabaseRequest(deleteRequest()), {
|
||||
ipcCallId: 1,
|
||||
operationId: 'operation-1',
|
||||
operationIdUnavailableReason: null,
|
||||
sourceEpochMs: 100,
|
||||
});
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
102,
|
||||
deleteMarker({
|
||||
boundary: 'end',
|
||||
outcome: 'success',
|
||||
sourceEpochMs: 101,
|
||||
})
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
const followUp = deleteMarker({
|
||||
ipcCallId: 2,
|
||||
operationId: null,
|
||||
sourceEpochMs: 103,
|
||||
});
|
||||
assert.equal(capture.acceptPreload(42, 104, followUp), true);
|
||||
assert.deepEqual(
|
||||
capture.matchDatabaseRequest(
|
||||
deleteRequest({ operationId: undefined })
|
||||
),
|
||||
{
|
||||
ipcCallId: 2,
|
||||
operationId: null,
|
||||
operationIdUnavailableReason: null,
|
||||
sourceEpochMs: 103,
|
||||
}
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
106,
|
||||
deleteMarker({
|
||||
...followUp,
|
||||
boundary: 'end',
|
||||
outcome: 'success',
|
||||
sourceEpochMs: 105,
|
||||
})
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(capture.stop().invalidReasons, []);
|
||||
});
|
||||
|
||||
it('quarantines overlapping deletes that share the explicit no-operation-ID key', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
const first = deleteMarker({ operationId: null });
|
||||
|
||||
assert.equal(capture.acceptPreload(42, 100, first), true);
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
101,
|
||||
deleteMarker({
|
||||
ipcCallId: 2,
|
||||
operationId: null,
|
||||
sourceEpochMs: 101,
|
||||
})
|
||||
),
|
||||
false
|
||||
);
|
||||
assert.deepEqual(
|
||||
capture.matchDatabaseRequest(deleteRequest({ operationId: null })),
|
||||
{
|
||||
ipcCallId: null,
|
||||
operationId: null,
|
||||
operationIdUnavailableReason:
|
||||
'xtream-preload-performance-marker-ambiguous',
|
||||
sourceEpochMs: null,
|
||||
}
|
||||
);
|
||||
assert.equal(capture.stop().quarantinedKeyCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
function deleteMarker(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
action: null,
|
||||
boundary: 'start',
|
||||
categoryType: null,
|
||||
contentType: null,
|
||||
ipcCallId: 1,
|
||||
itemCount: null,
|
||||
method: 'dbDeletePlaylist',
|
||||
operationId: 'operation-1',
|
||||
outcome: null,
|
||||
playlistId: 'playlist-1',
|
||||
schemaVersion: 1,
|
||||
sessionId: null,
|
||||
sourceEpochMs: 100,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function deleteRequest(
|
||||
payloadOverrides: Record<string, unknown> = {}
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
operation: 'DB_DELETE_PLAYLIST',
|
||||
payload: {
|
||||
operationId: 'operation-1',
|
||||
playlistId: 'playlist-1',
|
||||
...payloadOverrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamIterationDefinition,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { iteration } from './xtream-benchmark-report.fixtures';
|
||||
import { assertXtreamIterationResult } from './xtream-iteration-validity';
|
||||
import type {
|
||||
XtreamCancellationEvidence,
|
||||
XtreamRawIterationResult,
|
||||
} from './xtream-summary-contract';
|
||||
|
||||
describe('Xtream raw descriptor integrity', () => {
|
||||
it('rejects symbol and non-enumerable top-level extras', () => {
|
||||
for (const key of [Symbol('extra'), 'hiddenExtra'] as const) {
|
||||
const raw = createIteration();
|
||||
Object.defineProperty(raw, key, { value: true });
|
||||
|
||||
assert.throws(
|
||||
() => assertXtreamIterationResult(raw),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects symbol extras on cancellation records', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
Object.defineProperty(evidence.timeline[0], Symbol('extra'), {
|
||||
value: true,
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertXtreamIterationResult(raw),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a cancellation timeline accessor without invoking it', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
let invocations = 0;
|
||||
const accessorEvidence = {};
|
||||
Object.defineProperty(accessorEvidence, 'timeline', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
invocations += 1;
|
||||
return evidence.timeline;
|
||||
},
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellationEvidence:
|
||||
accessorEvidence as XtreamCancellationEvidence,
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
assert.equal(invocations, 0);
|
||||
});
|
||||
|
||||
it('rejects cancellation record accessors without invoking them', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
const original = evidence.timeline[0];
|
||||
let invocations = 0;
|
||||
const accessorRecord = { ...original };
|
||||
Object.defineProperty(accessorRecord, 'epochMs', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
invocations += 1;
|
||||
return original.epochMs;
|
||||
},
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellationEvidence: {
|
||||
timeline: [
|
||||
accessorRecord,
|
||||
...evidence.timeline.slice(1),
|
||||
],
|
||||
},
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
assert.equal(invocations, 0);
|
||||
});
|
||||
|
||||
it('rejects every own cancellation timeline key beyond dense indices and length', () => {
|
||||
for (const extraKey of [
|
||||
Symbol('extra'),
|
||||
'hiddenExtra',
|
||||
'map',
|
||||
] as const) {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
const timeline = [...evidence.timeline];
|
||||
Object.defineProperty(timeline, extraKey, {
|
||||
value:
|
||||
extraKey === 'map'
|
||||
? Array.prototype.map.bind(timeline)
|
||||
: true,
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellationEvidence: { timeline },
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function cancellationEvidence(
|
||||
raw: XtreamRawIterationResult
|
||||
): XtreamCancellationEvidence {
|
||||
assert.ok(raw.cancellationEvidence);
|
||||
return raw.cancellationEvidence;
|
||||
}
|
||||
|
||||
function createIteration(
|
||||
scenarioId: XtreamScenarioId = XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
): XtreamRawIterationResult {
|
||||
const definition: XtreamIterationDefinition = {
|
||||
eligibleForComparison: true,
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: 'run-01',
|
||||
scenarioId,
|
||||
};
|
||||
return iteration(definition, 1);
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
rm,
|
||||
symlink,
|
||||
unlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
|
||||
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
|
||||
import {
|
||||
assertXtreamDiagnosticArtifactEvidence,
|
||||
validateXtreamDiagnosticArtifacts,
|
||||
type XtreamDiagnosticCapture,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
|
||||
const HEAP_SNAPSHOT = {
|
||||
edges: [0, 0, 0],
|
||||
nodes: [0, 0, 1, 0, 1, 0],
|
||||
snapshot: {
|
||||
meta: {
|
||||
edge_fields: ['type', 'name_or_index', 'to_node'],
|
||||
edge_types: [['context'], 'string_or_number', 'node'],
|
||||
node_fields: [
|
||||
'type',
|
||||
'name',
|
||||
'id',
|
||||
'self_size',
|
||||
'edge_count',
|
||||
'detachedness',
|
||||
],
|
||||
node_types: [
|
||||
['hidden'],
|
||||
'string',
|
||||
'number',
|
||||
'number',
|
||||
'number',
|
||||
'number',
|
||||
],
|
||||
},
|
||||
},
|
||||
strings: ['', 'root'],
|
||||
};
|
||||
const RENDERER_TRACE = {
|
||||
traceEvents: [{ name: 'xtream-diagnostic', ph: 'X', ts: 1 }],
|
||||
};
|
||||
const directories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
});
|
||||
|
||||
describe('Xtream diagnostic artifacts', () => {
|
||||
it('accepts one complete main, renderer, and database-worker artifact set', async () => {
|
||||
const directory = await createDirectory('valid');
|
||||
await writeValidArtifacts(directory);
|
||||
|
||||
const evidence = await validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
);
|
||||
|
||||
assertXtreamDiagnosticArtifactEvidence(evidence);
|
||||
assert.equal(evidence.artifacts.length, 7);
|
||||
assert.match(evidence.directorySha256, /^[a-f0-9]{64}$/u);
|
||||
assert.ok(
|
||||
evidence.artifacts.every(
|
||||
({ bytes, filename, label, sha256 }) =>
|
||||
bytes > 0 &&
|
||||
filename.length > 0 &&
|
||||
label.length > 0 &&
|
||||
/^[a-f0-9]{64}$/u.test(sha256)
|
||||
)
|
||||
);
|
||||
assert.ok(Object.isFrozen(evidence));
|
||||
assert.ok(Object.isFrozen(evidence.artifacts));
|
||||
assert.ok(evidence.artifacts.every(Object.isFrozen));
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamDiagnosticArtifactEvidence(
|
||||
structuredClone(evidence)
|
||||
),
|
||||
/validated Xtream diagnostic artifact evidence/
|
||||
);
|
||||
});
|
||||
|
||||
it('fails closed for malformed or missing process artifacts', async () => {
|
||||
const directory = await createDirectory('invalid');
|
||||
await writeValidArtifacts(directory);
|
||||
await writeFile(join(directory, 'renderer.trace.json'), '{');
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
),
|
||||
/xtream-diagnostic-artifact-invalid:renderer-trace/
|
||||
);
|
||||
|
||||
await writeFile(
|
||||
join(directory, 'renderer.trace.json'),
|
||||
JSON.stringify(RENDERER_TRACE)
|
||||
);
|
||||
const missing: XtreamDiagnosticCapture = {
|
||||
...capture(directory),
|
||||
main: {
|
||||
...capture(directory).main,
|
||||
cpuProfilePath: null,
|
||||
},
|
||||
};
|
||||
await assert.rejects(
|
||||
() => validateXtreamDiagnosticArtifacts(missing, directory),
|
||||
/xtream-diagnostic-artifact-missing:main-cpu-profile/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects escaped paths and symlinks', async () => {
|
||||
const directory = await createDirectory('contained');
|
||||
const outside = await createDirectory('outside');
|
||||
await writeValidArtifacts(directory);
|
||||
await writeFile(
|
||||
join(outside, 'main.cpuprofile'),
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
);
|
||||
|
||||
const escaped: XtreamDiagnosticCapture = {
|
||||
...capture(directory),
|
||||
main: {
|
||||
...capture(directory).main,
|
||||
cpuProfilePath: join(outside, 'main.cpuprofile'),
|
||||
},
|
||||
};
|
||||
await assert.rejects(
|
||||
() => validateXtreamDiagnosticArtifacts(escaped, directory),
|
||||
/xtream-diagnostic-artifact-path-invalid:main-cpu-profile/
|
||||
);
|
||||
|
||||
const rendererProfile = join(directory, 'renderer.cpuprofile');
|
||||
await unlink(rendererProfile);
|
||||
await symlink(join(outside, 'main.cpuprofile'), rendererProfile);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
),
|
||||
/xtream-diagnostic-artifact-path-invalid:renderer-cpu-profile/
|
||||
);
|
||||
|
||||
const symlinkRoot = await createDirectory('directory-symlink');
|
||||
const realDirectory = join(symlinkRoot, 'real');
|
||||
const symlinkDirectory = join(symlinkRoot, 'alias');
|
||||
await mkdir(realDirectory);
|
||||
await writeValidArtifacts(realDirectory);
|
||||
await symlink(realDirectory, symlinkDirectory, 'dir');
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(symlinkDirectory),
|
||||
symlinkDirectory
|
||||
),
|
||||
/xtream-diagnostic-artifact-path-invalid:iteration-directory/
|
||||
);
|
||||
});
|
||||
|
||||
it('requires exactly one database worker and no playlist-refresh worker', async () => {
|
||||
const directory = await createDirectory('workers');
|
||||
await writeValidArtifacts(directory);
|
||||
await writeFile(
|
||||
join(directory, 'database.worker-9.cpuprofile'),
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
),
|
||||
/xtream-diagnostic-database-profile-cardinality-invalid/
|
||||
);
|
||||
|
||||
await unlink(join(directory, 'database.worker-9.cpuprofile'));
|
||||
await writeFile(
|
||||
join(directory, 'playlist-refresh.worker-2.cpuprofile'),
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
),
|
||||
/xtream-diagnostic-playlist-profile-unexpected/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects every rogue top-level profile, snapshot, and trace artifact', async () => {
|
||||
const filenames = [
|
||||
'main-extra.cpuprofile',
|
||||
'main-extra.heapsnapshot',
|
||||
'renderer-extra.cpuprofile',
|
||||
'renderer-extra.heapsnapshot',
|
||||
'renderer-extra.trace.json',
|
||||
'rogue.cpuprofile',
|
||||
'rogue.heapsnapshot',
|
||||
'rogue.trace.json',
|
||||
'database.worker-evil.cpuprofile',
|
||||
'database.worker-4.trace.json',
|
||||
'playlist-refresh.worker-2.trace.json',
|
||||
'rogue.CPUPROFILE',
|
||||
'rogue.HeapSnapshot',
|
||||
'rogue.TRACE.JSON',
|
||||
] as const;
|
||||
|
||||
for (const filename of filenames) {
|
||||
const directory = await createDirectory(filename);
|
||||
await writeValidArtifacts(directory);
|
||||
await writeFile(
|
||||
join(directory, filename),
|
||||
JSON.stringify(
|
||||
filename.endsWith('.cpuprofile')
|
||||
? CPU_PROFILE
|
||||
: filename.endsWith('.heapsnapshot')
|
||||
? HEAP_SNAPSHOT
|
||||
: RENDERER_TRACE
|
||||
)
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
),
|
||||
/xtream-diagnostic-artifact-cardinality-invalid/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects mixed-case rogue artifacts nested anywhere in the iteration tree', async () => {
|
||||
const directory = await createDirectory('nested-rogue');
|
||||
const nested = join(directory, 'nested');
|
||||
await writeValidArtifacts(directory);
|
||||
await mkdir(nested);
|
||||
await writeFile(
|
||||
join(nested, 'rogue.CpuProfile'),
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
),
|
||||
/^Error: xtream-diagnostic-artifact-cardinality-invalid$/
|
||||
);
|
||||
});
|
||||
|
||||
it('snapshots the validated database worker ordinal before constructing paths', async () => {
|
||||
const directory = await createDirectory('ordinal-getter');
|
||||
await writeValidArtifacts(directory);
|
||||
let reads = 0;
|
||||
const worker = {
|
||||
kind: 'database.worker',
|
||||
get ordinal(): number {
|
||||
reads += 1;
|
||||
return reads === 1 ? 4 : ('../../outside' as unknown as number);
|
||||
},
|
||||
profilePath: join(directory, 'database.worker-4.cpuprofile'),
|
||||
snapshotPath: join(directory, 'database.worker-4.heapsnapshot'),
|
||||
};
|
||||
const getterCapture: XtreamDiagnosticCapture = {
|
||||
...capture(directory),
|
||||
main: {
|
||||
...capture(directory).main,
|
||||
workers: [worker],
|
||||
},
|
||||
};
|
||||
|
||||
await assert.doesNotReject(() =>
|
||||
validateXtreamDiagnosticArtifacts(getterCapture, directory)
|
||||
);
|
||||
assert.equal(reads, 1);
|
||||
});
|
||||
|
||||
it('sanitizes capture Proxy failures before filesystem access', async () => {
|
||||
const secret = 'must-not-escape-diagnostic-capture';
|
||||
const directory = await createDirectory('proxy-capture');
|
||||
const captureProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys() {
|
||||
throw new Error(secret);
|
||||
},
|
||||
}
|
||||
) as XtreamDiagnosticCapture;
|
||||
|
||||
await assert.rejects(
|
||||
() => validateXtreamDiagnosticArtifacts(captureProxy, directory),
|
||||
(error: unknown) =>
|
||||
error instanceof Error &&
|
||||
error.message === 'xtream-diagnostic-capture-invalid' &&
|
||||
!error.message.includes(secret)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function capture(
|
||||
directory: string,
|
||||
databaseOrdinal = 4
|
||||
): XtreamDiagnosticCapture {
|
||||
return {
|
||||
main: {
|
||||
cpuProfilePath: join(directory, 'main.cpuprofile'),
|
||||
heapSnapshotPath: join(directory, 'main.heapsnapshot'),
|
||||
workers: [
|
||||
{
|
||||
kind: 'database.worker',
|
||||
ordinal: databaseOrdinal,
|
||||
profilePath: join(
|
||||
directory,
|
||||
`database.worker-${databaseOrdinal}.cpuprofile`
|
||||
),
|
||||
snapshotPath: join(
|
||||
directory,
|
||||
`database.worker-${databaseOrdinal}.heapsnapshot`
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
renderer: {
|
||||
cpuProfilePath: join(directory, 'renderer.cpuprofile'),
|
||||
heapSnapshotPath: join(directory, 'renderer.heapsnapshot'),
|
||||
tracePath: join(directory, 'renderer.trace.json'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createDirectory(label: string): Promise<string> {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), `iptvnator-xtream-diagnostic-${label}-`)
|
||||
);
|
||||
directories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
async function writeValidArtifacts(
|
||||
directory: string,
|
||||
databaseOrdinal = 4
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(directory, 'main.cpuprofile'),
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
),
|
||||
writeFile(
|
||||
join(directory, 'renderer.cpuprofile'),
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
),
|
||||
writeFile(
|
||||
join(directory, `database.worker-${databaseOrdinal}.cpuprofile`),
|
||||
JSON.stringify(CPU_PROFILE)
|
||||
),
|
||||
writeFile(
|
||||
join(directory, `database.worker-${databaseOrdinal}.heapsnapshot`),
|
||||
JSON.stringify(HEAP_SNAPSHOT)
|
||||
),
|
||||
writeFile(
|
||||
join(directory, 'main.heapsnapshot'),
|
||||
JSON.stringify(HEAP_SNAPSHOT)
|
||||
),
|
||||
writeFile(
|
||||
join(directory, 'renderer.heapsnapshot'),
|
||||
JSON.stringify(HEAP_SNAPSHOT)
|
||||
),
|
||||
writeFile(
|
||||
join(directory, 'renderer.trace.json'),
|
||||
JSON.stringify(RENDERER_TRACE)
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import { lstat, readdir, realpath } from 'node:fs/promises';
|
||||
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
|
||||
import {
|
||||
isCpuProfile,
|
||||
isHeapSnapshot,
|
||||
isRendererTrace,
|
||||
readCpuProfileDurationMicros,
|
||||
} from './initial-import-diagnostic-artifact-content-validation';
|
||||
import {
|
||||
assertXtreamManifest,
|
||||
assertXtreamManifestIdentity,
|
||||
} from './xtream-control-state-schema';
|
||||
import {
|
||||
snapshotXtreamDiagnosticCapture,
|
||||
type XtreamDiagnosticCapture,
|
||||
type XtreamDiagnosticWorkerArtifact,
|
||||
} from './xtream-diagnostic-capture-schema';
|
||||
import {
|
||||
assertXtreamArtifactRedacted,
|
||||
createXtreamDiagnosticEvidenceAuthority,
|
||||
serializeXtreamArtifactForPersistence,
|
||||
type XtreamDiagnosticArtifactEvidence,
|
||||
type XtreamValidatedArtifactContent,
|
||||
} from './xtream-diagnostic-evidence';
|
||||
import {
|
||||
isXtreamUnsafeFileError,
|
||||
readXtreamSafeRegularFile,
|
||||
} from './xtream-safe-file-read';
|
||||
|
||||
export {
|
||||
assertXtreamArtifactRedacted,
|
||||
assertXtreamManifest,
|
||||
assertXtreamManifestIdentity,
|
||||
serializeXtreamArtifactForPersistence,
|
||||
};
|
||||
export type {
|
||||
XtreamDiagnosticArtifactEvidence,
|
||||
XtreamDiagnosticArtifactReceipt,
|
||||
} from './xtream-diagnostic-evidence';
|
||||
export type { XtreamDiagnosticCapture } from './xtream-diagnostic-capture-schema';
|
||||
|
||||
interface ArtifactDescription {
|
||||
readonly capturesCpuProfileDuration: boolean;
|
||||
readonly filename: string;
|
||||
readonly label: string;
|
||||
readonly path: string | null;
|
||||
readonly validate: (value: unknown) => boolean;
|
||||
}
|
||||
|
||||
const EVIDENCE_AUTHORITY = createXtreamDiagnosticEvidenceAuthority();
|
||||
|
||||
export async function validateXtreamDiagnosticArtifacts(
|
||||
capture: XtreamDiagnosticCapture,
|
||||
iterationDirectory: string
|
||||
): Promise<XtreamDiagnosticArtifactEvidence> {
|
||||
const snapshot = snapshotXtreamDiagnosticCapture(capture);
|
||||
if (typeof iterationDirectory !== 'string') {
|
||||
throw artifactPathError('iteration-directory');
|
||||
}
|
||||
const directory = resolve(iterationDirectory);
|
||||
let realDirectory: string;
|
||||
try {
|
||||
const stats = await lstat(directory);
|
||||
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
||||
throw new Error('unsafe-iteration-directory');
|
||||
}
|
||||
realDirectory = await realpath(directory);
|
||||
} catch {
|
||||
throw artifactPathError('iteration-directory');
|
||||
}
|
||||
|
||||
const databaseWorker = requireExactDatabaseWorker(snapshot.main.workers);
|
||||
const artifacts: readonly ArtifactDescription[] = [
|
||||
artifact(
|
||||
'main.cpuprofile',
|
||||
'main-cpu-profile',
|
||||
snapshot.main.cpuProfilePath,
|
||||
isCpuProfile,
|
||||
true
|
||||
),
|
||||
artifact(
|
||||
'renderer.cpuprofile',
|
||||
'renderer-cpu-profile',
|
||||
snapshot.renderer.cpuProfilePath,
|
||||
isCpuProfile,
|
||||
true
|
||||
),
|
||||
artifact(
|
||||
'renderer.trace.json',
|
||||
'renderer-trace',
|
||||
snapshot.renderer.tracePath,
|
||||
isRendererTrace
|
||||
),
|
||||
artifact(
|
||||
'main.heapsnapshot',
|
||||
'main-heap-snapshot',
|
||||
snapshot.main.heapSnapshotPath,
|
||||
isHeapSnapshot
|
||||
),
|
||||
artifact(
|
||||
'renderer.heapsnapshot',
|
||||
'renderer-heap-snapshot',
|
||||
snapshot.renderer.heapSnapshotPath,
|
||||
isHeapSnapshot
|
||||
),
|
||||
artifact(
|
||||
`database.worker-${databaseWorker.ordinal}.cpuprofile`,
|
||||
'database-cpu-profile',
|
||||
databaseWorker.profilePath,
|
||||
isCpuProfile,
|
||||
true
|
||||
),
|
||||
artifact(
|
||||
`database.worker-${databaseWorker.ordinal}.heapsnapshot`,
|
||||
'database-heap-snapshot',
|
||||
databaseWorker.snapshotPath,
|
||||
isHeapSnapshot
|
||||
),
|
||||
];
|
||||
await assertWorkerArtifactCardinality(
|
||||
directory,
|
||||
artifacts.map(({ filename }) => filename)
|
||||
);
|
||||
const validated: XtreamValidatedArtifactContent[] = [];
|
||||
for (const description of artifacts) {
|
||||
validated.push(
|
||||
await validateJsonArtifact(directory, realDirectory, description)
|
||||
);
|
||||
}
|
||||
await assertWorkerArtifactCardinality(
|
||||
directory,
|
||||
artifacts.map(({ filename }) => filename)
|
||||
);
|
||||
return EVIDENCE_AUTHORITY.issue(
|
||||
realDirectory,
|
||||
validated,
|
||||
databaseWorker.ordinal
|
||||
);
|
||||
}
|
||||
|
||||
export function assertXtreamDiagnosticArtifactEvidence(
|
||||
value: unknown
|
||||
): asserts value is XtreamDiagnosticArtifactEvidence {
|
||||
if (!EVIDENCE_AUTHORITY.is(value)) {
|
||||
throw new Error(
|
||||
'expected validated Xtream diagnostic artifact evidence'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function requireExactDatabaseWorker(
|
||||
workers: readonly XtreamDiagnosticWorkerArtifact[]
|
||||
): XtreamDiagnosticWorkerArtifact {
|
||||
if (workers.some((worker) => worker.kind === 'playlist-refresh.worker')) {
|
||||
throw new Error('xtream-diagnostic-playlist-profile-unexpected');
|
||||
}
|
||||
const databaseWorkers = workers.filter(
|
||||
(worker) => worker.kind === 'database.worker'
|
||||
);
|
||||
const worker = databaseWorkers[0];
|
||||
if (
|
||||
databaseWorkers.length !== 1 ||
|
||||
workers.length !== 1 ||
|
||||
!worker ||
|
||||
!Number.isSafeInteger(worker.ordinal) ||
|
||||
worker.ordinal <= 0
|
||||
) {
|
||||
throw new Error(
|
||||
'xtream-diagnostic-database-profile-cardinality-invalid'
|
||||
);
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
async function assertWorkerArtifactCardinality(
|
||||
directory: string,
|
||||
expectedArtifacts: readonly string[]
|
||||
): Promise<void> {
|
||||
const entries = await listDiagnosticArtifactEntries(directory);
|
||||
if (
|
||||
entries.some((entry) =>
|
||||
/^playlist-refresh\.worker-\d+\.(?:cpuprofile|heapsnapshot)$/iu.test(
|
||||
entry
|
||||
)
|
||||
)
|
||||
) {
|
||||
throw new Error('xtream-diagnostic-playlist-profile-unexpected');
|
||||
}
|
||||
for (const extension of ['cpuprofile', 'heapsnapshot']) {
|
||||
if (
|
||||
entries.filter((entry) =>
|
||||
new RegExp(
|
||||
`^database\\.worker-\\d+\\.${extension}$`,
|
||||
'iu'
|
||||
).test(entry)
|
||||
).length !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
`xtream-diagnostic-database-${extension === 'cpuprofile' ? 'profile' : 'snapshot'}-cardinality-invalid`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
entries.length !== expectedArtifacts.length ||
|
||||
entries.some((entry) => !expectedArtifacts.includes(entry)) ||
|
||||
expectedArtifacts.some((entry) => !entries.includes(entry))
|
||||
) {
|
||||
throw new Error('xtream-diagnostic-artifact-cardinality-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
async function listDiagnosticArtifactEntries(
|
||||
directory: string
|
||||
): Promise<readonly string[]> {
|
||||
const artifacts: string[] = [];
|
||||
const walk = async (current: string): Promise<void> => {
|
||||
const entries = await readdir(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const path = join(current, entry.name);
|
||||
if (
|
||||
/(?:\.cpuprofile|\.heapsnapshot|\.trace\.json)$/iu.test(
|
||||
entry.name
|
||||
)
|
||||
) {
|
||||
artifacts.push(relative(directory, path));
|
||||
}
|
||||
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
||||
const stats = await lstat(path);
|
||||
if (stats.isSymbolicLink()) continue;
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error('unsafe-artifact-tree-entry');
|
||||
}
|
||||
await walk(path);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await walk(directory);
|
||||
return artifacts;
|
||||
} catch {
|
||||
throw artifactPathError('iteration-directory');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateJsonArtifact(
|
||||
directory: string,
|
||||
realDirectory: string,
|
||||
description: ArtifactDescription
|
||||
): Promise<XtreamValidatedArtifactContent> {
|
||||
if (description.path === null) {
|
||||
throw new Error(
|
||||
`xtream-diagnostic-artifact-missing:${description.label}`
|
||||
);
|
||||
}
|
||||
const expectedPath = join(directory, description.filename);
|
||||
if (resolve(description.path) !== expectedPath) {
|
||||
throw artifactPathError(description.label);
|
||||
}
|
||||
|
||||
let content: Buffer;
|
||||
let value: unknown;
|
||||
try {
|
||||
const snapshot = await readXtreamSafeRegularFile(expectedPath);
|
||||
const realArtifactPath = snapshot.realPath;
|
||||
if (!isContained(realDirectory, realArtifactPath)) {
|
||||
throw new Error('escaped-artifact');
|
||||
}
|
||||
content = snapshot.content;
|
||||
value = JSON.parse(content.toString('utf8')) as unknown;
|
||||
} catch (error) {
|
||||
if (
|
||||
isXtreamUnsafeFileError(error) ||
|
||||
(error instanceof Error && error.message === 'escaped-artifact')
|
||||
) {
|
||||
throw artifactPathError(description.label);
|
||||
}
|
||||
throw new Error(
|
||||
`xtream-diagnostic-artifact-invalid:${description.label}`
|
||||
);
|
||||
}
|
||||
if (!description.validate(value)) {
|
||||
throw new Error(
|
||||
`xtream-diagnostic-artifact-invalid:${description.label}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
content,
|
||||
cpuProfileDurationMicros: description.capturesCpuProfileDuration
|
||||
? readCpuProfileDurationMicros(value)
|
||||
: null,
|
||||
filename: description.filename,
|
||||
label: description.label,
|
||||
};
|
||||
}
|
||||
|
||||
function artifact(
|
||||
filename: string,
|
||||
label: string,
|
||||
path: string | null,
|
||||
validate: (value: unknown) => boolean,
|
||||
capturesCpuProfileDuration = false
|
||||
): ArtifactDescription {
|
||||
return {
|
||||
capturesCpuProfileDuration,
|
||||
filename,
|
||||
label,
|
||||
path,
|
||||
validate,
|
||||
};
|
||||
}
|
||||
|
||||
function isContained(directory: string, artifactPath: string): boolean {
|
||||
const relativePath = relative(directory, artifactPath);
|
||||
return (
|
||||
relativePath.length > 0 &&
|
||||
!isAbsolute(relativePath) &&
|
||||
relativePath !== '..' &&
|
||||
!relativePath.startsWith(`..${sep}`)
|
||||
);
|
||||
}
|
||||
|
||||
function artifactPathError(label: string): Error {
|
||||
return new Error(`xtream-diagnostic-artifact-path-invalid:${label}`);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
snapshotXtreamDiagnosticCapture,
|
||||
type XtreamDiagnosticWorkerArtifact,
|
||||
} from './xtream-diagnostic-capture-schema';
|
||||
|
||||
const WORKER: XtreamDiagnosticWorkerArtifact = {
|
||||
kind: 'database.worker',
|
||||
ordinal: 4,
|
||||
profilePath: '/tmp/database.worker-4.cpuprofile',
|
||||
snapshotPath: '/tmp/database.worker-4.heapsnapshot',
|
||||
};
|
||||
|
||||
describe('Xtream diagnostic capture schema', () => {
|
||||
it('reads a workers accessor once before validating and freezing it', () => {
|
||||
let reads = 0;
|
||||
const capture = {
|
||||
main: {
|
||||
cpuProfilePath: '/tmp/main.cpuprofile',
|
||||
heapSnapshotPath: '/tmp/main.heapsnapshot',
|
||||
get workers() {
|
||||
reads += 1;
|
||||
return [{ ...WORKER }];
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
cpuProfilePath: '/tmp/renderer.cpuprofile',
|
||||
heapSnapshotPath: '/tmp/renderer.heapsnapshot',
|
||||
tracePath: '/tmp/renderer.trace.json',
|
||||
},
|
||||
};
|
||||
|
||||
const snapshot = snapshotXtreamDiagnosticCapture(capture);
|
||||
|
||||
assert.equal(reads, 1);
|
||||
assert.deepEqual(snapshot.main.workers, [WORKER]);
|
||||
assert.ok(Object.isFrozen(snapshot.main.workers));
|
||||
assert.ok(Object.isFrozen(snapshot.main.workers[0]));
|
||||
});
|
||||
|
||||
it('rejects the producer-impossible zero worker ordinal', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
snapshotXtreamDiagnosticCapture({
|
||||
main: {
|
||||
cpuProfilePath: '/tmp/main.cpuprofile',
|
||||
heapSnapshotPath: '/tmp/main.heapsnapshot',
|
||||
workers: [{ ...WORKER, ordinal: 0 }],
|
||||
},
|
||||
renderer: {
|
||||
cpuProfilePath: '/tmp/renderer.cpuprofile',
|
||||
heapSnapshotPath: '/tmp/renderer.heapsnapshot',
|
||||
tracePath: '/tmp/renderer.trace.json',
|
||||
},
|
||||
}),
|
||||
/xtream-diagnostic-capture-invalid/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
export interface XtreamDiagnosticWorkerArtifact {
|
||||
readonly kind: string;
|
||||
readonly ordinal: number;
|
||||
readonly profilePath: string | null;
|
||||
readonly snapshotPath: string | null;
|
||||
}
|
||||
|
||||
export interface XtreamDiagnosticCapture {
|
||||
readonly main: {
|
||||
readonly cpuProfilePath: string | null;
|
||||
readonly heapSnapshotPath: string | null;
|
||||
readonly workers: readonly XtreamDiagnosticWorkerArtifact[];
|
||||
};
|
||||
readonly renderer: {
|
||||
readonly cpuProfilePath: string | null;
|
||||
readonly heapSnapshotPath: string | null;
|
||||
readonly tracePath: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export function snapshotXtreamDiagnosticCapture(
|
||||
value: unknown
|
||||
): XtreamDiagnosticCapture {
|
||||
try {
|
||||
return snapshotCapture(value);
|
||||
} catch {
|
||||
throw new Error('xtream-diagnostic-capture-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotCapture(value: unknown): XtreamDiagnosticCapture {
|
||||
if (!isExactRecord(value, ['main', 'renderer'])) invalid();
|
||||
const main = value['main'];
|
||||
const renderer = value['renderer'];
|
||||
const workerValues =
|
||||
typeof main === 'object' && main !== null
|
||||
? Reflect.get(main, 'workers')
|
||||
: null;
|
||||
if (
|
||||
!isExactRecord(main, [
|
||||
'cpuProfilePath',
|
||||
'heapSnapshotPath',
|
||||
'workers',
|
||||
]) ||
|
||||
!isExactRecord(renderer, [
|
||||
'cpuProfilePath',
|
||||
'heapSnapshotPath',
|
||||
'tracePath',
|
||||
]) ||
|
||||
!Array.isArray(workerValues)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const workers = snapshotWorkers(workerValues);
|
||||
const snapshot = {
|
||||
main: {
|
||||
cpuProfilePath: snapshotPath(main['cpuProfilePath']),
|
||||
heapSnapshotPath: snapshotPath(main['heapSnapshotPath']),
|
||||
workers: Object.freeze(workers),
|
||||
},
|
||||
renderer: {
|
||||
cpuProfilePath: snapshotPath(renderer['cpuProfilePath']),
|
||||
heapSnapshotPath: snapshotPath(renderer['heapSnapshotPath']),
|
||||
tracePath: snapshotPath(renderer['tracePath']),
|
||||
},
|
||||
};
|
||||
Object.freeze(snapshot.main);
|
||||
Object.freeze(snapshot.renderer);
|
||||
return Object.freeze(snapshot);
|
||||
}
|
||||
|
||||
function snapshotWorkers(value: unknown): XtreamDiagnosticWorkerArtifact[] {
|
||||
if (!Array.isArray(value)) invalid();
|
||||
const length = Reflect.get(value, 'length') as unknown;
|
||||
if (
|
||||
!Number.isSafeInteger(length) ||
|
||||
Number(length) < 0 ||
|
||||
Number(length) > 64
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const expected = new Set([
|
||||
'length',
|
||||
...Array.from({ length: Number(length) }, (_, index) => String(index)),
|
||||
]);
|
||||
const keys = Reflect.ownKeys(value);
|
||||
if (
|
||||
keys.length !== expected.size ||
|
||||
keys.some((key) => typeof key !== 'string' || !expected.has(key))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Array.from({ length: Number(length) }, (_, index) =>
|
||||
snapshotWorker(Reflect.get(value, String(index)))
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotWorker(value: unknown): XtreamDiagnosticWorkerArtifact {
|
||||
if (
|
||||
!isExactRecord(value, [
|
||||
'kind',
|
||||
'ordinal',
|
||||
'profilePath',
|
||||
'snapshotPath',
|
||||
])
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const kind = value['kind'];
|
||||
const ordinal = value['ordinal'];
|
||||
const profilePath = snapshotPath(value['profilePath']);
|
||||
const snapshotPathValue = snapshotPath(value['snapshotPath']);
|
||||
if (
|
||||
typeof kind !== 'string' ||
|
||||
!Number.isSafeInteger(ordinal) ||
|
||||
Number(ordinal) <= 0
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.freeze({
|
||||
kind,
|
||||
ordinal: ordinal as number,
|
||||
profilePath,
|
||||
snapshotPath: snapshotPathValue,
|
||||
});
|
||||
}
|
||||
|
||||
function snapshotPath(value: unknown): string | null {
|
||||
if (value !== null && typeof value !== 'string') invalid();
|
||||
return value as string | null;
|
||||
}
|
||||
|
||||
function isExactRecord(
|
||||
value: unknown,
|
||||
keys: readonly string[]
|
||||
): value is Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const actual = Reflect.ownKeys(value);
|
||||
return (
|
||||
actual.length === keys.length &&
|
||||
actual.every((key) => typeof key === 'string' && keys.includes(key))
|
||||
);
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid diagnostic capture');
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
assertXtreamArtifactRedacted,
|
||||
serializeXtreamArtifactForPersistence,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import type { XtreamArtifactSafetyContext } from './xtream-summary-contract';
|
||||
|
||||
const TOKEN = 'diagnostic-control-token';
|
||||
const SAFETY: XtreamArtifactSafetyContext = {
|
||||
controlToken: TOKEN,
|
||||
syntheticCredentials: {
|
||||
password: 'performance',
|
||||
username: 'performance',
|
||||
},
|
||||
};
|
||||
const SAFE_ERROR = 'Xtream artifact must remain credential-safe';
|
||||
|
||||
describe('Xtream diagnostic persistence safety', () => {
|
||||
it('rejects single- and double-percent-encoded synthetic credentials', () => {
|
||||
for (const endpoint of [
|
||||
'http://127.0.0.1/player_api.php?%75sername%3D%70erformance%26%70assword%3D%70erformance',
|
||||
'http://127.0.0.1/player_api.php?%2575sername%253D%2570erformance%2526%2570assword%253D%2570erformance',
|
||||
'http://%70erformance%3A%70erformance%40127.0.0.1/player_api.php',
|
||||
'http://%2570erformance%253A%2570erformance%2540127.0.0.1/player_api.php',
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
serializeXtreamArtifactForPersistence({ endpoint }, SAFETY),
|
||||
exactSafeError
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects triple encoding and fails closed beyond the decode cap', () => {
|
||||
const userInfo =
|
||||
'http://performance:performance@127.0.0.1/player_api.php';
|
||||
for (const endpoint of [
|
||||
encodeTimes(userInfo, 3),
|
||||
encodeTimes(userInfo, 9),
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
serializeXtreamArtifactForPersistence({ endpoint }, SAFETY),
|
||||
exactSafeError
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves benign malformed percent text', () => {
|
||||
assert.equal(
|
||||
serializeXtreamArtifactForPersistence(
|
||||
{ label: 'progress is 100% complete' },
|
||||
SAFETY
|
||||
),
|
||||
'{"label":"progress is 100% complete"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not let malformed percent text mask encoded credentials', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
serializeXtreamArtifactForPersistence(
|
||||
{
|
||||
endpoint:
|
||||
'http://%ZZ%70erformance%3A%70erformance%40127.0.0.1/player_api.php',
|
||||
},
|
||||
SAFETY
|
||||
),
|
||||
exactSafeError
|
||||
);
|
||||
});
|
||||
|
||||
it('does not trust an array own forEach during standalone redaction', () => {
|
||||
const unsafe = [
|
||||
'http://mock.invalid/player_api.php?username=performance',
|
||||
];
|
||||
Object.defineProperty(unsafe, 'forEach', {
|
||||
value: () => undefined,
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => assertXtreamArtifactRedacted(unsafe),
|
||||
exactSafeError
|
||||
);
|
||||
});
|
||||
|
||||
it('revalidates the serialized JSON emitted by toJSON hooks', () => {
|
||||
assert.equal(
|
||||
serializeXtreamArtifactForPersistence(
|
||||
{
|
||||
visible: true,
|
||||
toJSON() {
|
||||
return { visible: true };
|
||||
},
|
||||
},
|
||||
SAFETY
|
||||
),
|
||||
'{"visible":true}'
|
||||
);
|
||||
|
||||
for (const unsafe of [
|
||||
{
|
||||
toJSON() {
|
||||
return { username: 'injected-after-first-validation' };
|
||||
},
|
||||
},
|
||||
{
|
||||
toJSON() {
|
||||
return {
|
||||
endpoint:
|
||||
'http://mock.invalid/player_api.php?token=late',
|
||||
};
|
||||
},
|
||||
},
|
||||
]) {
|
||||
assert.throws(
|
||||
() => serializeXtreamArtifactForPersistence(unsafe, SAFETY),
|
||||
exactSafeError
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('sanitizes ownKeys, getter, and revoked Proxy failures', () => {
|
||||
const ownKeysProxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys() {
|
||||
throw new Error(TOKEN);
|
||||
},
|
||||
}
|
||||
);
|
||||
const safetyProxy = new Proxy(SAFETY, {
|
||||
get() {
|
||||
throw new Error(TOKEN);
|
||||
},
|
||||
});
|
||||
const getterValue = Object.defineProperty({}, 'visible', {
|
||||
enumerable: true,
|
||||
get() {
|
||||
throw new Error(TOKEN);
|
||||
},
|
||||
});
|
||||
const throwingJson = {
|
||||
toJSON() {
|
||||
throw new Error(TOKEN);
|
||||
},
|
||||
};
|
||||
const revocable = Proxy.revocable({}, {});
|
||||
revocable.revoke();
|
||||
|
||||
assert.throws(
|
||||
() => assertXtreamArtifactRedacted(ownKeysProxy, [TOKEN]),
|
||||
exactSafeError
|
||||
);
|
||||
assert.throws(
|
||||
() => assertXtreamArtifactRedacted(revocable.proxy, [TOKEN]),
|
||||
exactSafeError
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
serializeXtreamArtifactForPersistence(
|
||||
{ visible: true },
|
||||
safetyProxy
|
||||
),
|
||||
exactSafeError
|
||||
);
|
||||
assert.throws(
|
||||
() => serializeXtreamArtifactForPersistence(ownKeysProxy, SAFETY),
|
||||
exactSafeError
|
||||
);
|
||||
assert.throws(
|
||||
() => serializeXtreamArtifactForPersistence(getterValue, SAFETY),
|
||||
exactSafeError
|
||||
);
|
||||
assert.throws(
|
||||
() => serializeXtreamArtifactForPersistence(throwingJson, SAFETY),
|
||||
exactSafeError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function exactSafeError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error &&
|
||||
error.message === SAFE_ERROR &&
|
||||
!error.message.includes(TOKEN)
|
||||
);
|
||||
}
|
||||
|
||||
function encodeTimes(value: string, count: number): string {
|
||||
let encoded = value;
|
||||
for (let pass = 0; pass < count; pass += 1) {
|
||||
encoded = encodeURIComponent(encoded);
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type { XtreamArtifactSafetyContext } from './xtream-summary-contract';
|
||||
|
||||
export interface XtreamDiagnosticArtifactReceipt {
|
||||
readonly bytes: number;
|
||||
readonly filename: string;
|
||||
readonly label: string;
|
||||
readonly sha256: string;
|
||||
}
|
||||
|
||||
declare const EVIDENCE_BRAND: unique symbol;
|
||||
|
||||
export interface XtreamDiagnosticArtifactEvidence {
|
||||
readonly [EVIDENCE_BRAND]: true;
|
||||
readonly artifacts: readonly XtreamDiagnosticArtifactReceipt[];
|
||||
readonly directorySha256: string;
|
||||
readonly profiles: XtreamDiagnosticProfileEvidence;
|
||||
}
|
||||
|
||||
export interface XtreamDiagnosticProfileEvidence {
|
||||
readonly databaseWorker: {
|
||||
readonly durationMicros: number;
|
||||
readonly ordinal: number;
|
||||
};
|
||||
readonly main: {
|
||||
readonly durationMicros: number;
|
||||
};
|
||||
readonly renderer: {
|
||||
readonly durationMicros: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface XtreamValidatedArtifactContent {
|
||||
readonly content: Uint8Array;
|
||||
readonly cpuProfileDurationMicros: number | null;
|
||||
readonly filename: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
interface XtreamDiagnosticEvidenceAuthority {
|
||||
is(value: unknown): value is XtreamDiagnosticArtifactEvidence;
|
||||
issue(
|
||||
realDirectory: string,
|
||||
artifacts: readonly XtreamValidatedArtifactContent[],
|
||||
databaseWorkerOrdinal: number
|
||||
): XtreamDiagnosticArtifactEvidence;
|
||||
}
|
||||
|
||||
const SENSITIVE_KEYS = new Set([
|
||||
'authorization',
|
||||
'credential',
|
||||
'credentials',
|
||||
'password',
|
||||
'playlisturl',
|
||||
'serverurl',
|
||||
'token',
|
||||
'username',
|
||||
]);
|
||||
const MAX_PERCENT_DECODE_PASSES = 8;
|
||||
|
||||
export function createXtreamDiagnosticEvidenceAuthority(): XtreamDiagnosticEvidenceAuthority {
|
||||
const validated = new WeakSet<object>();
|
||||
return {
|
||||
is(value): value is XtreamDiagnosticArtifactEvidence {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
validated.has(value)
|
||||
);
|
||||
},
|
||||
issue(realDirectory, artifacts, databaseWorkerOrdinal) {
|
||||
const receipts = Object.freeze(
|
||||
artifacts.map(({ content, filename, label }) =>
|
||||
Object.freeze({
|
||||
bytes: content.byteLength,
|
||||
filename,
|
||||
label,
|
||||
sha256: sha256(content),
|
||||
})
|
||||
)
|
||||
);
|
||||
const duration = (label: string): number => {
|
||||
const value = artifacts.find(
|
||||
(artifact) => artifact.label === label
|
||||
)?.cpuProfileDurationMicros;
|
||||
if (!Number.isSafeInteger(value) || Number(value) <= 0) {
|
||||
throw new Error('invalid diagnostic profile evidence');
|
||||
}
|
||||
return Number(value);
|
||||
};
|
||||
if (
|
||||
!Number.isSafeInteger(databaseWorkerOrdinal) ||
|
||||
databaseWorkerOrdinal <= 0
|
||||
) {
|
||||
throw new Error('invalid diagnostic worker identity');
|
||||
}
|
||||
const profiles = Object.freeze({
|
||||
databaseWorker: Object.freeze({
|
||||
durationMicros: duration('database-cpu-profile'),
|
||||
ordinal: databaseWorkerOrdinal,
|
||||
}),
|
||||
main: Object.freeze({
|
||||
durationMicros: duration('main-cpu-profile'),
|
||||
}),
|
||||
renderer: Object.freeze({
|
||||
durationMicros: duration('renderer-cpu-profile'),
|
||||
}),
|
||||
});
|
||||
const directorySha256 = sha256(
|
||||
Buffer.from(
|
||||
`${realDirectory}\0${JSON.stringify(receipts)}`,
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
const evidence = Object.freeze({
|
||||
artifacts: receipts,
|
||||
directorySha256,
|
||||
profiles,
|
||||
}) as XtreamDiagnosticArtifactEvidence;
|
||||
validated.add(evidence);
|
||||
return evidence;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function assertXtreamArtifactRedacted(
|
||||
value: unknown,
|
||||
forbiddenValues: readonly string[] = []
|
||||
): void {
|
||||
try {
|
||||
assertArtifactRedacted(value, forbiddenValues);
|
||||
} catch {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
}
|
||||
|
||||
function assertArtifactRedacted(
|
||||
value: unknown,
|
||||
forbiddenValues: readonly string[]
|
||||
): void {
|
||||
const secrets: string[] = [];
|
||||
for (let index = 0; index < forbiddenValues.length; index += 1) {
|
||||
const entry = Reflect.get(forbiddenValues, String(index)) as unknown;
|
||||
if (typeof entry === 'string' && entry.length > 0) {
|
||||
secrets.push(entry);
|
||||
}
|
||||
}
|
||||
const visit = (input: unknown): void => {
|
||||
if (typeof input === 'string') {
|
||||
for (const candidate of decodedVariants(input)) {
|
||||
if (
|
||||
secrets.some((secret) => candidate.includes(secret)) ||
|
||||
/[?&](?:username|password|token)=/iu.test(candidate) ||
|
||||
/:\/\/[^/\s]+@/u.test(candidate)
|
||||
) {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
const length = input.length;
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
visit(Reflect.get(input, String(index)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!input || typeof input !== 'object') return;
|
||||
for (const [key, entry] of Object.entries(input)) {
|
||||
if (SENSITIVE_KEYS.has(key.replace(/[-_]/gu, '').toLowerCase())) {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
visit(entry);
|
||||
}
|
||||
};
|
||||
visit(value);
|
||||
}
|
||||
|
||||
function decodedVariants(value: string): readonly string[] {
|
||||
const variants = [value];
|
||||
let current = value;
|
||||
for (let pass = 0; pass < MAX_PERCENT_DECODE_PASSES; pass += 1) {
|
||||
const decoded = decodePercentPass(current);
|
||||
if (decoded === current) break;
|
||||
variants.push(decoded);
|
||||
current = decoded;
|
||||
}
|
||||
const afterCap = decodePercentPass(current);
|
||||
if (afterCap !== current) throw unsafeArtifact();
|
||||
return variants;
|
||||
}
|
||||
|
||||
function decodePercentPass(value: string): string {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value.replace(/%([a-f0-9]{2})/giu, (_match, hex: string) =>
|
||||
String.fromCharCode(Number.parseInt(hex, 16))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeXtreamArtifactForPersistence(
|
||||
value: unknown,
|
||||
safety: XtreamArtifactSafetyContext
|
||||
): string {
|
||||
try {
|
||||
return serializeArtifact(value, safety);
|
||||
} catch {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
}
|
||||
|
||||
function serializeArtifact(
|
||||
value: unknown,
|
||||
safety: XtreamArtifactSafetyContext
|
||||
): string {
|
||||
const controlToken = safety.controlToken;
|
||||
const credentials = safety.syntheticCredentials;
|
||||
const username = credentials?.username;
|
||||
const password = credentials?.password;
|
||||
if (
|
||||
typeof controlToken !== 'string' ||
|
||||
controlToken.length === 0 ||
|
||||
username !== 'performance' ||
|
||||
password !== 'performance'
|
||||
) {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
assertXtreamArtifactRedacted(value, [controlToken]);
|
||||
let serialized: string | undefined;
|
||||
try {
|
||||
serialized = JSON.stringify(value);
|
||||
} catch {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
if (typeof serialized !== 'string') throw unsafeArtifact();
|
||||
let canonical: unknown;
|
||||
try {
|
||||
canonical = JSON.parse(serialized) as unknown;
|
||||
} catch {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
assertXtreamArtifactRedacted(canonical, [controlToken]);
|
||||
if (
|
||||
JSON.stringify(canonical) !== serialized ||
|
||||
serialized.includes(controlToken) ||
|
||||
/performance(?::|%3a)performance/iu.test(serialized) ||
|
||||
/[?&](?:username=performance[^"#]*&password=performance|password=performance[^"#]*&username=performance)/iu.test(
|
||||
serialized
|
||||
) ||
|
||||
/:\/\/performance(?::|%3a)performance@/iu.test(serialized)
|
||||
) {
|
||||
throw unsafeArtifact();
|
||||
}
|
||||
return serialized;
|
||||
}
|
||||
|
||||
function sha256(value: Uint8Array): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function unsafeArtifact(): Error {
|
||||
return new Error('Xtream artifact must remain credential-safe');
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
createXtreamIterationDefinitions,
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { iteration } from './xtream-benchmark-report.fixtures';
|
||||
import { assertXtreamIterationResult } from './xtream-iteration-validity';
|
||||
import type { XtreamRawIterationResult } from './xtream-summary-contract';
|
||||
import { validXtreamWorkerRequestIdentity } from './xtream-worker-request-identity';
|
||||
|
||||
test('enforces producer-coherent request identities and source-to-worker chronology', () => {
|
||||
const raw = createRaw(XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE);
|
||||
assert.doesNotThrow(() => assertXtreamIterationResult(raw));
|
||||
|
||||
const correlated = request(raw, 'DB_GET_CATEGORIES');
|
||||
const nonApplicable = request(raw, 'DB_GET_APP_STATE');
|
||||
const requiredOperationId = request(raw, 'DB_SAVE_CONTENT');
|
||||
const receivedEpochMs = requireEpoch(correlated.requestReceivedEpochMs);
|
||||
|
||||
assert.equal(
|
||||
validXtreamWorkerRequestIdentity({
|
||||
...correlated,
|
||||
sourceEpochMs: receivedEpochMs + 0.006,
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
validXtreamWorkerRequestIdentity({
|
||||
...correlated,
|
||||
sourceEpochMs: receivedEpochMs + 1,
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
assert.equal(nonApplicable.ipcCallId, null);
|
||||
assert.equal(nonApplicable.operationId, null);
|
||||
assert.equal(
|
||||
nonApplicable.operationIdUnavailableReason,
|
||||
'preload-performance-marker-not-applicable'
|
||||
);
|
||||
assert.equal(nonApplicable.sourceEpochMs, null);
|
||||
|
||||
const invalidRequests = [
|
||||
replaceRequest(raw, correlated, {
|
||||
ipcCallId: null,
|
||||
operationIdUnavailableReason:
|
||||
'preload-performance-marker-not-applicable',
|
||||
sourceEpochMs: null,
|
||||
}),
|
||||
replaceRequest(raw, nonApplicable, {
|
||||
ipcCallId: 900_001,
|
||||
operationId: 'forged-operation',
|
||||
operationIdUnavailableReason: null,
|
||||
sourceEpochMs:
|
||||
requireEpoch(nonApplicable.requestReceivedEpochMs) - 1,
|
||||
}),
|
||||
replaceRequest(raw, requiredOperationId, {
|
||||
operationId: null,
|
||||
operationIdUnavailableReason:
|
||||
'preload-performance-marker-uncorrelated',
|
||||
}),
|
||||
replaceRequest(raw, correlated, {
|
||||
operationId: 'forged-operation',
|
||||
operationIdUnavailableReason: null,
|
||||
}),
|
||||
replaceRequest(raw, correlated, {
|
||||
sourceEpochMs: receivedEpochMs + 1,
|
||||
}),
|
||||
];
|
||||
|
||||
for (const requests of invalidRequests) {
|
||||
assert.throws(
|
||||
() => assertXtreamIterationResult(withRequests(raw, requests)),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('binds populated and empty playlist deletes to their real producer identities', () => {
|
||||
const raw = createRaw(XTREAM_SCENARIO_ID.DELETE_LARGE);
|
||||
const deletes = raw.databaseWorker.requests.evidence.filter(
|
||||
({ operation }) => operation === 'DB_DELETE_PLAYLIST'
|
||||
);
|
||||
const populated = deletes.find(
|
||||
(candidate) =>
|
||||
JSON.stringify(requestItemCounts(candidate)) ===
|
||||
JSON.stringify([100_100, 100_101])
|
||||
);
|
||||
const emptyFollowUp = deletes.find(
|
||||
(candidate) =>
|
||||
JSON.stringify(requestItemCounts(candidate)) ===
|
||||
JSON.stringify([0, 1])
|
||||
);
|
||||
assert.ok(populated?.operationId);
|
||||
assert.ok(emptyFollowUp);
|
||||
|
||||
const producerCoherent = replaceRequest(raw, emptyFollowUp, {
|
||||
operationId: null,
|
||||
operationIdUnavailableReason: null,
|
||||
});
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamIterationResult(withRequests(raw, producerCoherent))
|
||||
);
|
||||
|
||||
const missingPopulatedIdentity = replaceRequest(raw, populated, {
|
||||
operationId: null,
|
||||
operationIdUnavailableReason: null,
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult(
|
||||
withRequests(raw, missingPopulatedIdentity)
|
||||
),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
const fabricatedFollowUpIdentity = replaceRequest(raw, emptyFollowUp, {
|
||||
operationId: 'fabricated-follow-up-operation',
|
||||
operationIdUnavailableReason: null,
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult(
|
||||
withRequests(raw, fabricatedFollowUpIdentity)
|
||||
),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
test('requires producer IPC IDs to preserve per-producer source chronology', () => {
|
||||
const raw = createRaw(XTREAM_SCENARIO_ID.DELETE_LARGE);
|
||||
const [first, second] = raw.databaseWorker.requests.evidence;
|
||||
assert.ok(first);
|
||||
assert.ok(second);
|
||||
assert.ok(first.ipcCallId !== null);
|
||||
assert.ok(second.ipcCallId !== null);
|
||||
assert.ok(first.sourceEpochMs !== null);
|
||||
assert.ok(second.sourceEpochMs !== null);
|
||||
const laterSourceEpochMs = first.sourceEpochMs + 0.25;
|
||||
const chronologicalRequests = raw.databaseWorker.requests.evidence.map(
|
||||
(candidate) =>
|
||||
candidate === second
|
||||
? { ...candidate, sourceEpochMs: laterSourceEpochMs }
|
||||
: candidate
|
||||
);
|
||||
const chronologicalIpcSpans = raw.phaseCapture.ipcSpans.map((span) =>
|
||||
span.ipcCallId === second.ipcCallId
|
||||
? { ...span, sourceEpochMs: laterSourceEpochMs }
|
||||
: span
|
||||
);
|
||||
const swapped = chronologicalRequests.map((candidate) => {
|
||||
if (candidate === first) {
|
||||
return { ...candidate, ipcCallId: second.ipcCallId };
|
||||
}
|
||||
return candidate.ipcCallId === second.ipcCallId
|
||||
? { ...candidate, ipcCallId: first.ipcCallId }
|
||||
: candidate;
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...withRequests(raw, swapped),
|
||||
phaseCapture: {
|
||||
...raw.phaseCapture,
|
||||
ipcSpans: chronologicalIpcSpans,
|
||||
},
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
const gapped = raw.databaseWorker.requests.evidence.map(
|
||||
(candidate, index) => ({
|
||||
...candidate,
|
||||
ipcCallId: index === 0 ? 10 : 30,
|
||||
})
|
||||
);
|
||||
const gappedIpcSpans = raw.phaseCapture.ipcSpans.map((span) => ({
|
||||
...span,
|
||||
ipcCallId: span.ipcCallId === first.ipcCallId ? 10 : 30,
|
||||
}));
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamIterationResult({
|
||||
...withRequests(raw, gapped),
|
||||
phaseCapture: {
|
||||
...raw.phaseCapture,
|
||||
ipcSpans: gappedIpcSpans,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const tied = raw.databaseWorker.requests.evidence.map((candidate) =>
|
||||
candidate === second
|
||||
? { ...candidate, sourceEpochMs: first.sourceEpochMs }
|
||||
: candidate
|
||||
);
|
||||
const tiedIpcSpans = raw.phaseCapture.ipcSpans.map((span) =>
|
||||
span.ipcCallId === second.ipcCallId
|
||||
? { ...span, sourceEpochMs: first.sourceEpochMs as number }
|
||||
: span
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamIterationResult({
|
||||
...withRequests(raw, tied),
|
||||
phaseCapture: {
|
||||
...raw.phaseCapture,
|
||||
ipcSpans: tiedIpcSpans,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('does not let caller-owned evidence methods hide a missing populated delete identity', () => {
|
||||
const raw = createRaw(XTREAM_SCENARIO_ID.DELETE_LARGE);
|
||||
const pristine = raw.databaseWorker.requests.evidence;
|
||||
const populated = pristine.find(
|
||||
(candidate) =>
|
||||
candidate.operation === 'DB_DELETE_PLAYLIST' &&
|
||||
JSON.stringify(requestItemCounts(candidate)) ===
|
||||
JSON.stringify([100_100, 100_101])
|
||||
);
|
||||
assert.ok(populated);
|
||||
const forged = replaceRequest(raw, populated, {
|
||||
operationId: null,
|
||||
operationIdUnavailableReason: null,
|
||||
});
|
||||
const invoked: string[] = [];
|
||||
for (const method of ['filter', 'flatMap', 'map', 'some'] as const) {
|
||||
Object.defineProperty(forged, method, {
|
||||
configurable: true,
|
||||
value(...args: unknown[]) {
|
||||
invoked.push(method);
|
||||
return Reflect.apply(Array.prototype[method], pristine, args);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
assert.throws(
|
||||
() => assertXtreamIterationResult(withRequests(raw, forged)),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
assert.deepEqual(invoked, []);
|
||||
});
|
||||
|
||||
test('requires the database-worker isolate heap to fit inside main-process RSS only', () => {
|
||||
const raw = createRaw(XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
peakHeapUsedBytes: raw.main.peakRssBytes + 1,
|
||||
},
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
peakExternalBytes: raw.main.peakRssBytes + 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
function createRaw(scenarioId: XtreamScenarioId): XtreamRawIterationResult {
|
||||
const definition = createXtreamIterationDefinitions(false).find(
|
||||
(candidate) => candidate.scenarioId === scenarioId
|
||||
);
|
||||
assert.ok(definition);
|
||||
return iteration(definition, 1);
|
||||
}
|
||||
|
||||
function request(raw: XtreamRawIterationResult, operation: string) {
|
||||
const value = raw.databaseWorker.requests.evidence.find(
|
||||
(candidate) => candidate.operation === operation
|
||||
);
|
||||
assert.ok(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
function replaceRequest(
|
||||
raw: XtreamRawIterationResult,
|
||||
target: (typeof raw.databaseWorker.requests.evidence)[number],
|
||||
replacement: Partial<typeof target>
|
||||
) {
|
||||
return raw.databaseWorker.requests.evidence.map((candidate) =>
|
||||
candidate === target ? { ...candidate, ...replacement } : candidate
|
||||
);
|
||||
}
|
||||
|
||||
function withRequests(
|
||||
raw: XtreamRawIterationResult,
|
||||
evidence: typeof raw.databaseWorker.requests.evidence
|
||||
): XtreamRawIterationResult {
|
||||
return {
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
requests: { ...raw.databaseWorker.requests, evidence },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function requireEpoch(value: number | null): number {
|
||||
assert.ok(value !== null);
|
||||
return value;
|
||||
}
|
||||
|
||||
function requestItemCounts(
|
||||
request: XtreamRawIterationResult['databaseWorker']['requests']['evidence'][number]
|
||||
): readonly (number | null)[] {
|
||||
return request.phaseEvents
|
||||
.filter(({ boundary }) => boundary === 'end')
|
||||
.map(({ metadata }) => metadata?.itemCount ?? null);
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
type XtreamIterationDefinition,
|
||||
} from './xtream-benchmark-contract';
|
||||
import {
|
||||
XTREAM_IDENTIFIER,
|
||||
XTREAM_SHA256,
|
||||
} from './xtream-benchmark-manifest-schema';
|
||||
import {
|
||||
XTREAM_NOT_APPLICABLE,
|
||||
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',
|
||||
] as const;
|
||||
return (
|
||||
isExactRecord(value, keys) &&
|
||||
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'])
|
||||
);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
|
||||
import { writeXtreamExclusiveArtifact } from './xtream-exclusive-artifact-writer';
|
||||
|
||||
const directories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
});
|
||||
|
||||
describe('Xtream exclusive artifact writer', () => {
|
||||
it('durably writes exact bytes and returns their receipt', async () => {
|
||||
const directory = await createDirectory();
|
||||
const path = join(directory, 'raw.json');
|
||||
const content = '{"safe":true}';
|
||||
|
||||
const receipt = await writeXtreamExclusiveArtifact(path, content);
|
||||
|
||||
assert.deepEqual(receipt, {
|
||||
absolutePath: path,
|
||||
bytes: Buffer.byteLength(content),
|
||||
sha256: sha256(content),
|
||||
});
|
||||
assert.equal(await readFile(path, 'utf8'), content);
|
||||
});
|
||||
|
||||
it('never truncates or replaces an existing artifact', async () => {
|
||||
const directory = await createDirectory();
|
||||
const path = join(directory, 'raw.json');
|
||||
await writeFile(path, 'original', 'utf8');
|
||||
|
||||
await assert.rejects(
|
||||
writeXtreamExclusiveArtifact(path, 'replacement'),
|
||||
/Xtream artifact path is not exclusively writable/
|
||||
);
|
||||
assert.equal(await readFile(path, 'utf8'), 'original');
|
||||
});
|
||||
|
||||
it('rejects a final-component symlink and relative paths', async () => {
|
||||
const directory = await createDirectory();
|
||||
const target = join(directory, 'target.json');
|
||||
const alias = join(directory, 'alias.json');
|
||||
await writeFile(target, 'target', 'utf8');
|
||||
await symlink(target, alias);
|
||||
|
||||
await assert.rejects(
|
||||
writeXtreamExclusiveArtifact(alias, 'replacement'),
|
||||
/Xtream artifact path is not exclusively writable/
|
||||
);
|
||||
await assert.rejects(
|
||||
writeXtreamExclusiveArtifact('relative.json', '{}'),
|
||||
/Xtream artifact path is not exclusively writable/
|
||||
);
|
||||
assert.equal(await readFile(target, 'utf8'), 'target');
|
||||
});
|
||||
|
||||
it('rejects empty payloads before creating a file', async () => {
|
||||
const directory = await createDirectory();
|
||||
const path = join(directory, 'empty.json');
|
||||
|
||||
await assert.rejects(
|
||||
writeXtreamExclusiveArtifact(path, ''),
|
||||
/Xtream artifact path is not exclusively writable/
|
||||
);
|
||||
await assert.rejects(readFile(path), { code: 'ENOENT' });
|
||||
});
|
||||
});
|
||||
|
||||
async function createDirectory(): Promise<string> {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-xtream-exclusive-')
|
||||
);
|
||||
directories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
function sha256(value: string): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { open } from 'node:fs/promises';
|
||||
import { isAbsolute } from 'node:path';
|
||||
|
||||
import type { XtreamPersistenceReceipt } from './xtream-summary-contract';
|
||||
|
||||
export async function writeXtreamExclusiveArtifact(
|
||||
absolutePath: string,
|
||||
serialized: string
|
||||
): Promise<XtreamPersistenceReceipt> {
|
||||
try {
|
||||
if (
|
||||
typeof absolutePath !== 'string' ||
|
||||
!isAbsolute(absolutePath) ||
|
||||
typeof serialized !== 'string' ||
|
||||
serialized.length === 0
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const content = Buffer.from(serialized, 'utf8');
|
||||
const handle = await open(absolutePath, 'wx', 0o600);
|
||||
try {
|
||||
await handle.writeFile(content);
|
||||
await handle.sync();
|
||||
const stats = await handle.stat();
|
||||
if (
|
||||
!stats.isFile() ||
|
||||
stats.isSymbolicLink() ||
|
||||
stats.nlink !== 1 ||
|
||||
stats.size !== content.byteLength
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
return Object.freeze({
|
||||
absolutePath,
|
||||
bytes: content.byteLength,
|
||||
sha256: createHash('sha256').update(content).digest('hex'),
|
||||
});
|
||||
} catch {
|
||||
throw new Error('Xtream artifact path is not exclusively writable');
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { link, mkdtemp, rm, unlink, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, it } from 'node:test';
|
||||
|
||||
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
|
||||
import {
|
||||
validateXtreamDiagnosticArtifacts,
|
||||
type XtreamDiagnosticCapture,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import { verifyXtreamPersistenceReceipt } from './xtream-raw-persistence-receipt';
|
||||
|
||||
const HEAP_SNAPSHOT = {
|
||||
edges: [0, 0, 0],
|
||||
nodes: [0, 0, 1, 0, 1, 0],
|
||||
snapshot: {
|
||||
meta: {
|
||||
edge_fields: ['type', 'name_or_index', 'to_node'],
|
||||
edge_types: [['context'], 'string_or_number', 'node'],
|
||||
node_fields: [
|
||||
'type',
|
||||
'name',
|
||||
'id',
|
||||
'self_size',
|
||||
'edge_count',
|
||||
'detachedness',
|
||||
],
|
||||
node_types: [
|
||||
['hidden'],
|
||||
'string',
|
||||
'number',
|
||||
'number',
|
||||
'number',
|
||||
'number',
|
||||
],
|
||||
},
|
||||
},
|
||||
strings: ['', 'root'],
|
||||
};
|
||||
const RENDERER_TRACE = {
|
||||
traceEvents: [{ name: 'xtream-diagnostic', ph: 'X', ts: 1 }],
|
||||
};
|
||||
const directories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true }))
|
||||
);
|
||||
});
|
||||
|
||||
describe('Xtream filesystem evidence integrity', () => {
|
||||
it('rejects a raw persistence file with an external hard-link alias', async () => {
|
||||
const directory = await createDirectory('raw');
|
||||
const outside = await createDirectory('raw-outside');
|
||||
const serialized = '{"safe":true}';
|
||||
const rawPath = join(directory, 'raw.json');
|
||||
const externalAlias = join(outside, 'external-alias.json');
|
||||
await writeFile(rawPath, serialized, 'utf8');
|
||||
await link(rawPath, externalAlias);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
verifyXtreamPersistenceReceipt(serialized, {
|
||||
absolutePath: rawPath,
|
||||
bytes: Buffer.byteLength(serialized),
|
||||
sha256: sha256(serialized),
|
||||
}),
|
||||
/^Error: invalid Xtream persistence receipt$/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a diagnostic artifact with an external hard-link alias', async () => {
|
||||
const directory = await createDirectory('diagnostic');
|
||||
const outside = await createDirectory('outside');
|
||||
await writeValidArtifacts(directory);
|
||||
const tracePath = join(directory, 'renderer.trace.json');
|
||||
const externalTrace = join(outside, 'renderer.trace.json');
|
||||
await unlink(tracePath);
|
||||
await writeFile(externalTrace, JSON.stringify(RENDERER_TRACE));
|
||||
await link(externalTrace, tracePath);
|
||||
|
||||
await assert.rejects(
|
||||
() =>
|
||||
validateXtreamDiagnosticArtifacts(
|
||||
capture(directory),
|
||||
directory
|
||||
),
|
||||
/^Error: xtream-diagnostic-artifact-path-invalid:renderer-trace$/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function capture(directory: string): XtreamDiagnosticCapture {
|
||||
return {
|
||||
main: {
|
||||
cpuProfilePath: join(directory, 'main.cpuprofile'),
|
||||
heapSnapshotPath: join(directory, 'main.heapsnapshot'),
|
||||
workers: [
|
||||
{
|
||||
kind: 'database.worker',
|
||||
ordinal: 4,
|
||||
profilePath: join(
|
||||
directory,
|
||||
'database.worker-4.cpuprofile'
|
||||
),
|
||||
snapshotPath: join(
|
||||
directory,
|
||||
'database.worker-4.heapsnapshot'
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
renderer: {
|
||||
cpuProfilePath: join(directory, 'renderer.cpuprofile'),
|
||||
heapSnapshotPath: join(directory, 'renderer.heapsnapshot'),
|
||||
tracePath: join(directory, 'renderer.trace.json'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createDirectory(label: string): Promise<string> {
|
||||
const directory = await mkdtemp(
|
||||
join(tmpdir(), `iptvnator-xtream-fs-${label}-`)
|
||||
);
|
||||
directories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
async function writeValidArtifacts(directory: string): Promise<void> {
|
||||
await Promise.all([
|
||||
writeJson(join(directory, 'main.cpuprofile'), CPU_PROFILE),
|
||||
writeJson(join(directory, 'renderer.cpuprofile'), CPU_PROFILE),
|
||||
writeJson(join(directory, 'database.worker-4.cpuprofile'), CPU_PROFILE),
|
||||
writeJson(
|
||||
join(directory, 'database.worker-4.heapsnapshot'),
|
||||
HEAP_SNAPSHOT
|
||||
),
|
||||
writeJson(join(directory, 'main.heapsnapshot'), HEAP_SNAPSHOT),
|
||||
writeJson(join(directory, 'renderer.heapsnapshot'), HEAP_SNAPSHOT),
|
||||
writeJson(join(directory, 'renderer.trace.json'), RENDERER_TRACE),
|
||||
]);
|
||||
}
|
||||
|
||||
function writeJson(path: string, value: unknown): Promise<void> {
|
||||
return writeFile(path, JSON.stringify(value));
|
||||
}
|
||||
|
||||
function sha256(value: string): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
XTREAM_SCENARIO_ID,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { iteration } from './xtream-benchmark-report.fixtures';
|
||||
import {
|
||||
xtreamIpcProducerIdentityKey,
|
||||
xtreamIpcProducerNamespaceForMethod,
|
||||
xtreamIpcProducerNamespaceForWorkerOperation,
|
||||
} from './xtream-ipc-producer-identity';
|
||||
import { attributeXtreamPhases } from './xtream-phase-attribution';
|
||||
import { scenarioCapture } from './xtream-phase-attribution.fixtures';
|
||||
import { assertXtreamIterationResult } from './xtream-iteration-validity';
|
||||
|
||||
const DB_METHOD_OPERATION = new Map([
|
||||
['dbDeletePlaylist', 'DB_DELETE_PLAYLIST'],
|
||||
['dbDeleteXtreamContent', 'DB_DELETE_XTREAM_CONTENT'],
|
||||
['dbGetAppPlaylist', 'DB_GET_APP_PLAYLIST'],
|
||||
['dbGetCategories', 'DB_GET_CATEGORIES'],
|
||||
['dbGetContent', 'DB_GET_CONTENT'],
|
||||
['dbRestoreXtreamUserData', 'DB_RESTORE_XTREAM_USER_DATA'],
|
||||
['dbSaveCategories', 'DB_SAVE_CATEGORIES'],
|
||||
['dbSaveContent', 'DB_SAVE_CONTENT'],
|
||||
['dbUpsertAppPlaylist', 'DB_UPSERT_APP_PLAYLIST'],
|
||||
]);
|
||||
|
||||
describe('Xtream IPC attribution producer identity', () => {
|
||||
it('requires exact pairs, distinct call IDs, and source chronology', () => {
|
||||
const initial = scenarioCapture(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
);
|
||||
const requests = initial.ipcSpans
|
||||
.filter(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbGetCategories'
|
||||
)
|
||||
.sort((left, right) => left.ipcCallId - right.ipcCallId);
|
||||
const first = requests[0];
|
||||
const second = requests[1];
|
||||
assert.ok(first);
|
||||
assert.ok(second);
|
||||
const pairDrift = initial.ipcSpans.map((span) =>
|
||||
span.correlationId === first.correlationId &&
|
||||
span.boundary === 'response'
|
||||
? { ...span, ipcCallId: span.ipcCallId + 1 }
|
||||
: span
|
||||
);
|
||||
const sourcePairDrift = initial.ipcSpans.map((span) =>
|
||||
span.correlationId === first.correlationId &&
|
||||
span.boundary === 'response'
|
||||
? { ...span, sourceEpochMs: span.sourceEpochMs + 0.01 }
|
||||
: span
|
||||
);
|
||||
const duplicate = initial.ipcSpans.map((span) =>
|
||||
span.correlationId === second.correlationId
|
||||
? { ...span, ipcCallId: first.ipcCallId }
|
||||
: span
|
||||
);
|
||||
const chronologyRegression = initial.ipcSpans.map((span) =>
|
||||
span.correlationId === first.correlationId
|
||||
? { ...span, sourceEpochMs: 100.75 }
|
||||
: span
|
||||
);
|
||||
|
||||
for (const ipcSpans of [
|
||||
pairDrift,
|
||||
sourcePairDrift,
|
||||
duplicate,
|
||||
chronologyRegression,
|
||||
]) {
|
||||
assert.throws(
|
||||
() => attributeXtreamPhases({ ...initial, ipcSpans }),
|
||||
/invalid Xtream phase capture/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts one numeric call ID in each independent preload producer namespace', () => {
|
||||
const raw = iteration(
|
||||
{
|
||||
eligibleForComparison: true,
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: 'run-01',
|
||||
scenarioId: XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE,
|
||||
},
|
||||
1
|
||||
);
|
||||
const legacy = raw.phaseCapture.ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbUpsertAppPlaylist'
|
||||
);
|
||||
const xtream = raw.phaseCapture.ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbGetCategories'
|
||||
);
|
||||
assert.ok(legacy);
|
||||
assert.ok(xtream);
|
||||
assert.equal(legacy.ipcCallId, xtream.ipcCallId);
|
||||
const legacySourceEpochMs = legacy.sourceEpochMs + 0.25;
|
||||
const ipcSpans = raw.phaseCapture.ipcSpans.map((span) => {
|
||||
if (
|
||||
xtreamIpcProducerNamespaceForMethod(span.method) ===
|
||||
'legacy-preload'
|
||||
) {
|
||||
return { ...span, sourceEpochMs: legacySourceEpochMs };
|
||||
}
|
||||
return span.correlationId === xtream.correlationId
|
||||
? { ...span, ipcCallId: legacy.ipcCallId }
|
||||
: span;
|
||||
});
|
||||
const requests = raw.databaseWorker.requests.evidence.map((request) => {
|
||||
if (
|
||||
xtreamIpcProducerNamespaceForWorkerOperation(
|
||||
request.operation ?? ''
|
||||
) === 'legacy-preload'
|
||||
) {
|
||||
return { ...request, sourceEpochMs: legacySourceEpochMs };
|
||||
}
|
||||
return request.ipcCallId === xtream.ipcCallId &&
|
||||
request.operation === 'DB_GET_CATEGORIES'
|
||||
? { ...request, ipcCallId: legacy.ipcCallId }
|
||||
: request;
|
||||
});
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
requests: {
|
||||
...raw.databaseWorker.requests,
|
||||
evidence: requests,
|
||||
},
|
||||
},
|
||||
phaseCapture: { ...raw.phaseCapture, ipcSpans },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does not collide a cancellation call with the legacy preload namespace', () => {
|
||||
const raw = iteration(
|
||||
{
|
||||
eligibleForComparison: true,
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: 'run-01',
|
||||
scenarioId: XTREAM_SCENARIO_ID.CANCEL_IMPORT,
|
||||
},
|
||||
1
|
||||
);
|
||||
const cancellation = raw.phaseCapture.ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbCancelOperation'
|
||||
);
|
||||
const legacy = raw.phaseCapture.ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbUpsertAppPlaylist'
|
||||
);
|
||||
assert.ok(cancellation);
|
||||
assert.ok(legacy);
|
||||
const ipcSpans = raw.phaseCapture.ipcSpans.map((span) =>
|
||||
span.correlationId === legacy.correlationId
|
||||
? { ...span, ipcCallId: cancellation.ipcCallId }
|
||||
: span
|
||||
);
|
||||
const requests = raw.databaseWorker.requests.evidence.map((request) =>
|
||||
request.operation === 'DB_UPSERT_APP_PLAYLIST' &&
|
||||
request.ipcCallId === legacy.ipcCallId
|
||||
? { ...request, ipcCallId: cancellation.ipcCallId }
|
||||
: request
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
requests: {
|
||||
...raw.databaseWorker.requests,
|
||||
evidence: requests,
|
||||
},
|
||||
},
|
||||
phaseCapture: { ...raw.phaseCapture, ipcSpans },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('shares each database preload identity with its worker request', () => {
|
||||
for (const scenarioId of Object.values(XTREAM_SCENARIO_ID)) {
|
||||
const raw = iteration(
|
||||
{
|
||||
eligibleForComparison: true,
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: 'run-01',
|
||||
scenarioId,
|
||||
},
|
||||
1
|
||||
);
|
||||
const workersByProducerIdentity = new Map(
|
||||
raw.databaseWorker.requests.evidence.flatMap((request) =>
|
||||
request.ipcCallId === null
|
||||
? []
|
||||
: [
|
||||
[
|
||||
workerIdentityKey(
|
||||
request.operation ?? '',
|
||||
request.ipcCallId
|
||||
),
|
||||
request,
|
||||
] as const,
|
||||
]
|
||||
)
|
||||
);
|
||||
for (const span of raw.phaseCapture.ipcSpans) {
|
||||
if (span.boundary !== 'request') continue;
|
||||
const operation = DB_METHOD_OPERATION.get(span.method);
|
||||
if (!operation) continue;
|
||||
const namespace = xtreamIpcProducerNamespaceForMethod(
|
||||
span.method
|
||||
);
|
||||
assert.ok(namespace);
|
||||
const worker = workersByProducerIdentity.get(
|
||||
xtreamIpcProducerIdentityKey(namespace, span.ipcCallId)
|
||||
);
|
||||
assert.ok(worker);
|
||||
assert.equal(worker.operation, operation);
|
||||
assert.equal(worker.sourceEpochMs, span.sourceEpochMs);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function workerIdentityKey(operation: string, ipcCallId: number): string {
|
||||
const namespace = xtreamIpcProducerNamespaceForWorkerOperation(operation);
|
||||
assert.ok(namespace);
|
||||
return xtreamIpcProducerIdentityKey(namespace, ipcCallId);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
export interface XtreamIpcMarkerCaptureOptions {
|
||||
readonly actions: readonly string[];
|
||||
readonly mainPhases: readonly string[];
|
||||
readonly methods: readonly string[];
|
||||
readonly schemaVersion: number;
|
||||
}
|
||||
|
||||
export interface XtreamIpcRequestIdentity {
|
||||
ipcCallId: number | null;
|
||||
operationId: string | null;
|
||||
operationIdUnavailableReason: string | null;
|
||||
sourceEpochMs: number | null;
|
||||
}
|
||||
|
||||
export interface XtreamIpcCallState {
|
||||
completionOutcome: 'error' | 'success' | null;
|
||||
downstreamMatched: boolean;
|
||||
ended: boolean;
|
||||
key: string;
|
||||
marker: import('./xtream-ipc-marker-protocol').SafeXtreamPreloadMarker;
|
||||
quarantined: boolean;
|
||||
receivedEpochMs: number;
|
||||
}
|
||||
|
||||
export interface XtreamMainPhaseState {
|
||||
endEpochMs: number | null;
|
||||
startEpochMs: number;
|
||||
}
|
||||
|
||||
export interface XtreamIpcTimelineRecord {
|
||||
readonly action?: string | null;
|
||||
readonly boundary: 'end' | 'start';
|
||||
readonly categoryType?: string | null;
|
||||
readonly contentType?: string | null;
|
||||
readonly durationMs?: number | null;
|
||||
readonly epochMs: number;
|
||||
readonly ipcCallId?: number;
|
||||
readonly itemCount?: number | null;
|
||||
readonly method?: string;
|
||||
readonly operationId?: string | null;
|
||||
readonly outcome?: 'error' | 'success' | null;
|
||||
readonly phase?: string;
|
||||
readonly playlistId?: string | null;
|
||||
readonly requestId?: string;
|
||||
readonly sessionId?: string | null;
|
||||
readonly sourceEpochMs?: number;
|
||||
readonly type: 'xtream-main-phase' | 'xtream-preload-marker';
|
||||
}
|
||||
|
||||
export interface XtreamIpcMarkerCaptureSnapshot {
|
||||
readonly invalidReasons: readonly string[];
|
||||
readonly quarantinedKeyCount: number;
|
||||
readonly timeline: readonly XtreamIpcTimelineRecord[];
|
||||
}
|
||||
|
||||
export interface XtreamIpcMarkerCapture {
|
||||
acceptMainPhase(input: unknown): boolean;
|
||||
acceptPreload(
|
||||
senderWebContentsId: number,
|
||||
receivedEpochMs: number,
|
||||
input: unknown
|
||||
): boolean;
|
||||
matchDatabaseCancel(input: unknown): XtreamIpcRequestIdentity | null;
|
||||
matchDatabaseRequest(input: unknown): XtreamIpcRequestIdentity | null;
|
||||
start(expectedWebContentsId: number, captureStartedEpochMs: number): void;
|
||||
stop(): XtreamIpcMarkerCaptureSnapshot;
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import {
|
||||
XTREAM_MAIN_PERFORMANCE_PHASE,
|
||||
XTREAM_PRELOAD_PERFORMANCE_METHOD,
|
||||
XtreamCodeActions,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
import {
|
||||
createXtreamIpcMarkerCapture,
|
||||
type XtreamIpcMarkerCaptureOptions,
|
||||
} from './xtream-ipc-marker-events';
|
||||
import { createXtreamIpcMarkerProtocol } from './xtream-ipc-marker-protocol';
|
||||
import { createXtreamMainLifecycleValidator } from './xtream-main-lifecycle';
|
||||
|
||||
const options: XtreamIpcMarkerCaptureOptions = {
|
||||
actions: Object.values(XtreamCodeActions),
|
||||
mainPhases: Object.values(XTREAM_MAIN_PERFORMANCE_PHASE),
|
||||
methods: Object.values(XTREAM_PRELOAD_PERFORMANCE_METHOD),
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
function marker(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
sourceEpochMs: 100,
|
||||
ipcCallId: 1,
|
||||
method: 'dbSaveContent',
|
||||
boundary: 'start',
|
||||
outcome: null,
|
||||
playlistId: 'playlist-1',
|
||||
operationId: 'operation-1',
|
||||
sessionId: null,
|
||||
action: null,
|
||||
categoryType: null,
|
||||
contentType: 'live',
|
||||
itemCount: 60_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Xtream preload marker capture', () => {
|
||||
it('restores both factories without module closures for Electron-main injection', () => {
|
||||
const restore = <T>(source: string): T =>
|
||||
new Function(`"use strict"; return (${source});`)() as T;
|
||||
const protocolFactory = restore<typeof createXtreamIpcMarkerProtocol>(
|
||||
createXtreamIpcMarkerProtocol.toString()
|
||||
);
|
||||
const captureFactory = restore<typeof createXtreamIpcMarkerCapture>(
|
||||
createXtreamIpcMarkerCapture.toString()
|
||||
);
|
||||
const lifecycleFactory = restore<
|
||||
typeof createXtreamMainLifecycleValidator
|
||||
>(createXtreamMainLifecycleValidator.toString());
|
||||
const capture = captureFactory(
|
||||
options,
|
||||
protocolFactory(options),
|
||||
lifecycleFactory()
|
||||
);
|
||||
|
||||
capture.start(42, 90);
|
||||
assert.equal(capture.acceptPreload(42, 101, marker()), true);
|
||||
});
|
||||
|
||||
it('accepts only the exact 13-key safe schema from the bound webContents', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
|
||||
assert.equal(capture.acceptPreload(42, 101, marker()), true);
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
102,
|
||||
marker({
|
||||
boundary: 'end',
|
||||
outcome: 'success',
|
||||
sourceEpochMs: 101,
|
||||
})
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
const snapshot = capture.stop();
|
||||
assert.deepEqual(snapshot.invalidReasons, []);
|
||||
assert.equal(snapshot.quarantinedKeyCount, 0);
|
||||
assert.equal(snapshot.timeline.length, 2);
|
||||
assert.deepEqual(snapshot.timeline[0], {
|
||||
action: null,
|
||||
boundary: 'start',
|
||||
categoryType: null,
|
||||
contentType: 'live',
|
||||
epochMs: 101,
|
||||
ipcCallId: 1,
|
||||
itemCount: 60_000,
|
||||
method: 'dbSaveContent',
|
||||
operationId: 'operation-1',
|
||||
outcome: null,
|
||||
playlistId: 'playlist-1',
|
||||
sessionId: null,
|
||||
sourceEpochMs: 100,
|
||||
type: 'xtream-preload-marker',
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes sub-millisecond cross-process clock skew and rejects larger reversals', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 100, marker({ sourceEpochMs: 100.006 })),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
101,
|
||||
marker({
|
||||
boundary: 'end',
|
||||
outcome: 'success',
|
||||
sourceEpochMs: 101.006,
|
||||
})
|
||||
),
|
||||
true
|
||||
);
|
||||
const snapshot = capture.stop();
|
||||
assert.deepEqual(snapshot.invalidReasons, []);
|
||||
assert.equal(snapshot.timeline[0]?.epochMs, 100.006);
|
||||
assert.equal(snapshot.timeline[1]?.epochMs, 101.006);
|
||||
|
||||
const reversed = createXtreamIpcMarkerCapture(options);
|
||||
reversed.start(42, 90);
|
||||
assert.equal(
|
||||
reversed.acceptPreload(42, 100, marker({ sourceEpochMs: 101.001 })),
|
||||
false
|
||||
);
|
||||
assert.ok(
|
||||
reversed
|
||||
.stop()
|
||||
.invalidReasons.includes('preload-marker-source-after-receipt')
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects delayed preload markers from a completed capture generation', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
capture.stop();
|
||||
capture.start(42, 200);
|
||||
|
||||
assert.equal(capture.acceptPreload(42, 210, marker()), false);
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
211,
|
||||
marker({
|
||||
boundary: 'end',
|
||||
outcome: 'success',
|
||||
sourceEpochMs: 101,
|
||||
})
|
||||
),
|
||||
false
|
||||
);
|
||||
assert.ok(
|
||||
capture
|
||||
.stop()
|
||||
.invalidReasons.includes('preload-marker-before-capture-start')
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects another sender, extra or missing keys, malformed IDs, and regressing epochs', () => {
|
||||
const invalidCases: Array<{
|
||||
first?: Record<string, unknown>;
|
||||
input: Record<string, unknown>;
|
||||
receivedEpochMs?: number;
|
||||
sender?: number;
|
||||
}> = [
|
||||
{ input: marker(), sender: 7 },
|
||||
{ input: { ...marker(), password: 'provider-secret' } },
|
||||
{
|
||||
input: Object.fromEntries(
|
||||
Object.entries(marker()).filter(
|
||||
([key]) => key !== 'itemCount'
|
||||
)
|
||||
),
|
||||
},
|
||||
{ input: marker({ playlistId: ' playlist-1' }) },
|
||||
{ input: marker({ ipcCallId: 0 }) },
|
||||
{ input: marker({ outcome: 'success' }) },
|
||||
{
|
||||
first: marker(),
|
||||
input: marker({
|
||||
boundary: 'end',
|
||||
outcome: 'success',
|
||||
sourceEpochMs: 99,
|
||||
}),
|
||||
receivedEpochMs: 102,
|
||||
},
|
||||
{
|
||||
input: marker({ sourceEpochMs: 102 }),
|
||||
receivedEpochMs: 101,
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of invalidCases) {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
if (testCase.first) {
|
||||
assert.equal(
|
||||
capture.acceptPreload(42, 101, testCase.first),
|
||||
true
|
||||
);
|
||||
}
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
testCase.sender ?? 42,
|
||||
testCase.receivedEpochMs ?? 101,
|
||||
testCase.input
|
||||
),
|
||||
false
|
||||
);
|
||||
assert.notEqual(capture.stop().invalidReasons.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
it('correlates DB requests FIFO and quarantines overlapping duplicate keys', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
assert.equal(capture.acceptPreload(42, 100, marker()), true);
|
||||
|
||||
const identity = capture.matchDatabaseRequest({
|
||||
operation: 'DB_SAVE_CONTENT',
|
||||
payload: {
|
||||
operationId: 'operation-1',
|
||||
playlistId: 'playlist-1',
|
||||
type: 'live',
|
||||
},
|
||||
});
|
||||
assert.deepEqual(identity, {
|
||||
ipcCallId: 1,
|
||||
operationId: 'operation-1',
|
||||
operationIdUnavailableReason: null,
|
||||
sourceEpochMs: 100,
|
||||
});
|
||||
assert.equal(
|
||||
capture.matchDatabaseRequest({
|
||||
operation: 'DB_GET_APP_PLAYLIST',
|
||||
payload: { playlistId: 'playlist-1' },
|
||||
}),
|
||||
null
|
||||
);
|
||||
|
||||
const duplicateCapture = createXtreamIpcMarkerCapture(options);
|
||||
duplicateCapture.start(42, 90);
|
||||
assert.equal(duplicateCapture.acceptPreload(42, 100, marker()), true);
|
||||
assert.equal(
|
||||
duplicateCapture.acceptPreload(
|
||||
42,
|
||||
101,
|
||||
marker({ ipcCallId: 2, sourceEpochMs: 101 })
|
||||
),
|
||||
false
|
||||
);
|
||||
assert.deepEqual(
|
||||
duplicateCapture.matchDatabaseRequest({
|
||||
operation: 'DB_SAVE_CONTENT',
|
||||
payload: {
|
||||
operationId: 'operation-1',
|
||||
playlistId: 'playlist-1',
|
||||
type: 'live',
|
||||
},
|
||||
}),
|
||||
{
|
||||
ipcCallId: null,
|
||||
operationId: null,
|
||||
operationIdUnavailableReason:
|
||||
'xtream-preload-performance-marker-ambiguous',
|
||||
sourceEpochMs: null,
|
||||
}
|
||||
);
|
||||
assert.equal(duplicateCapture.stop().quarantinedKeyCount, 1);
|
||||
});
|
||||
|
||||
it('retains a mutable missing identity when a valid preload start arrives after the worker request', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
const identity = capture.matchDatabaseRequest({
|
||||
operation: 'DB_SAVE_CONTENT',
|
||||
payload: {
|
||||
operationId: 'operation-1',
|
||||
playlistId: 'playlist-1',
|
||||
type: 'live',
|
||||
},
|
||||
});
|
||||
assert.deepEqual(identity, {
|
||||
ipcCallId: null,
|
||||
operationId: null,
|
||||
operationIdUnavailableReason:
|
||||
'xtream-preload-performance-marker-missing',
|
||||
sourceEpochMs: null,
|
||||
});
|
||||
|
||||
assert.equal(capture.acceptPreload(42, 100, marker()), true);
|
||||
assert.deepEqual(identity, {
|
||||
ipcCallId: 1,
|
||||
operationId: 'operation-1',
|
||||
operationIdUnavailableReason: null,
|
||||
sourceEpochMs: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('correlates read-only content requests without inventing an operation ID', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
100,
|
||||
marker({
|
||||
ipcCallId: 3,
|
||||
itemCount: null,
|
||||
method: 'dbGetContent',
|
||||
operationId: null,
|
||||
})
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
capture.matchDatabaseRequest({
|
||||
operation: 'DB_GET_CONTENT',
|
||||
payload: { playlistId: 'playlist-1', type: 'live' },
|
||||
}),
|
||||
{
|
||||
ipcCallId: 3,
|
||||
operationId: null,
|
||||
operationIdUnavailableReason: null,
|
||||
sourceEpochMs: 100,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('correlates the preload cancel call separately from the active DB request', () => {
|
||||
const capture = createXtreamIpcMarkerCapture(options);
|
||||
capture.start(42, 90);
|
||||
const cancelMarker = marker({
|
||||
contentType: null,
|
||||
ipcCallId: 4,
|
||||
itemCount: null,
|
||||
method: 'dbCancelOperation',
|
||||
playlistId: null,
|
||||
});
|
||||
assert.equal(capture.acceptPreload(42, 100, cancelMarker), true);
|
||||
assert.deepEqual(
|
||||
capture.matchDatabaseCancel({
|
||||
operationId: 'operation-1',
|
||||
type: 'cancel',
|
||||
}),
|
||||
{
|
||||
ipcCallId: 4,
|
||||
operationId: 'operation-1',
|
||||
operationIdUnavailableReason: null,
|
||||
sourceEpochMs: 100,
|
||||
}
|
||||
);
|
||||
assert.equal(
|
||||
capture.acceptPreload(
|
||||
42,
|
||||
102,
|
||||
marker({
|
||||
...cancelMarker,
|
||||
boundary: 'end',
|
||||
outcome: 'success',
|
||||
sourceEpochMs: 101,
|
||||
})
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.deepEqual(capture.stop().invalidReasons, []);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,399 @@
|
||||
import {
|
||||
createXtreamIpcMarkerProtocol,
|
||||
type XtreamIpcMarkerProtocol,
|
||||
} from './xtream-ipc-marker-protocol';
|
||||
import type {
|
||||
XtreamIpcCallState,
|
||||
XtreamIpcMarkerCapture,
|
||||
XtreamIpcMarkerCaptureOptions,
|
||||
XtreamIpcRequestIdentity,
|
||||
XtreamIpcTimelineRecord,
|
||||
XtreamMainPhaseState,
|
||||
} from './xtream-ipc-marker-events.model';
|
||||
import {
|
||||
createXtreamMainLifecycleValidator,
|
||||
type XtreamMainLifecycleValidator,
|
||||
} from './xtream-main-lifecycle';
|
||||
export { deriveXtreamAcquisitionDurationMs } from './xtream-acquisition-duration';
|
||||
export type * from './xtream-ipc-marker-events.model';
|
||||
export function createXtreamIpcMarkerCapture(
|
||||
options: XtreamIpcMarkerCaptureOptions,
|
||||
protocol: XtreamIpcMarkerProtocol = createXtreamIpcMarkerProtocol(options),
|
||||
mainLifecycle: XtreamMainLifecycleValidator = createXtreamMainLifecycleValidator()
|
||||
): XtreamIpcMarkerCapture {
|
||||
let active = false;
|
||||
let captureStartedEpochMs = 0;
|
||||
let expectedWebContentsId: number | null = null;
|
||||
let lastMainEpochMs = 0;
|
||||
let lastReceivedEpochMs = 0;
|
||||
let lastSourceEpochMs = 0;
|
||||
let timeline: XtreamIpcTimelineRecord[] = [];
|
||||
let invalidReasons: string[] = [];
|
||||
const preloadSourceReceiptSkewToleranceMs = 1;
|
||||
const calls = new Map<number, XtreamIpcCallState>();
|
||||
const activeByKey = new Map<string, XtreamIpcCallState>();
|
||||
const pendingRequests = new Map<string, XtreamIpcRequestIdentity[]>();
|
||||
const quarantinedKeys = new Set<string>();
|
||||
const mainPhases = new Map<string, XtreamMainPhaseState>();
|
||||
const mainRequestCalls = new Map<string, XtreamIpcCallState>();
|
||||
const helpers = {
|
||||
invalidate(reason: string): false {
|
||||
invalidReasons.push(reason);
|
||||
return false;
|
||||
},
|
||||
mainPhasesForRequest(
|
||||
requestId: string
|
||||
): readonly (XtreamMainPhaseState & { readonly phase: string })[] {
|
||||
const prefix = `${requestId}\u0000`;
|
||||
return [...mainPhases.entries()]
|
||||
.filter(([key]) => key.startsWith(prefix))
|
||||
.map(([key, state]) => ({
|
||||
...state,
|
||||
phase: key.slice(prefix.length),
|
||||
}));
|
||||
},
|
||||
matchKey(
|
||||
key: string | null | undefined
|
||||
): XtreamIpcRequestIdentity | null {
|
||||
if (key === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (key === null || quarantinedKeys.has(key)) {
|
||||
return protocol.unavailableIdentity(
|
||||
'xtream-preload-performance-marker-ambiguous'
|
||||
);
|
||||
}
|
||||
const call = activeByKey.get(key);
|
||||
if (call && !call.downstreamMatched && !call.quarantined) {
|
||||
call.downstreamMatched = true;
|
||||
const identity = protocol.unavailableIdentity(
|
||||
'xtream-preload-performance-marker-missing'
|
||||
);
|
||||
protocol.applyMarkerIdentity(identity, call.marker);
|
||||
return identity;
|
||||
}
|
||||
const identity = protocol.unavailableIdentity(
|
||||
'xtream-preload-performance-marker-missing'
|
||||
);
|
||||
const waiting = pendingRequests.get(key) ?? [];
|
||||
waiting.push(identity);
|
||||
pendingRequests.set(key, waiting);
|
||||
if (waiting.length > 1) {
|
||||
helpers.quarantine(key);
|
||||
}
|
||||
return identity;
|
||||
},
|
||||
quarantine(key: string): void {
|
||||
quarantinedKeys.add(key);
|
||||
const activeCall = activeByKey.get(key);
|
||||
if (activeCall) {
|
||||
activeCall.quarantined = true;
|
||||
activeByKey.delete(key);
|
||||
}
|
||||
for (const identity of pendingRequests.get(key) ?? []) {
|
||||
Object.assign(
|
||||
identity,
|
||||
protocol.unavailableIdentity(
|
||||
'xtream-preload-performance-marker-ambiguous'
|
||||
)
|
||||
);
|
||||
}
|
||||
pendingRequests.delete(key);
|
||||
},
|
||||
};
|
||||
return {
|
||||
acceptMainPhase(input) {
|
||||
if (!active) return false;
|
||||
const event = protocol.parseMainPhase(input);
|
||||
if (!event) {
|
||||
return helpers.invalidate('main-phase-invalid');
|
||||
}
|
||||
const { boundary, durationMs, epochMs, phase, requestId } = event;
|
||||
if (epochMs < captureStartedEpochMs) {
|
||||
return helpers.invalidate('main-phase-before-capture-start');
|
||||
}
|
||||
if (epochMs < lastMainEpochMs) {
|
||||
return helpers.invalidate('main-phase-invalid');
|
||||
}
|
||||
lastMainEpochMs = epochMs;
|
||||
const boundaryKey = `${requestId}\u0000${phase}`;
|
||||
const phaseState = mainPhases.get(boundaryKey);
|
||||
if (boundary === 'start') {
|
||||
if (phaseState) {
|
||||
return helpers.invalidate('main-phase-duplicate-boundary');
|
||||
}
|
||||
mainPhases.set(boundaryKey, {
|
||||
endEpochMs: null,
|
||||
startEpochMs: epochMs,
|
||||
});
|
||||
} else {
|
||||
if (
|
||||
!phaseState ||
|
||||
phaseState.endEpochMs !== null ||
|
||||
epochMs < phaseState.startEpochMs
|
||||
) {
|
||||
return helpers.invalidate('main-phase-duplicate-boundary');
|
||||
}
|
||||
phaseState.endEpochMs = epochMs;
|
||||
}
|
||||
let call = mainRequestCalls.get(requestId);
|
||||
if (
|
||||
!call &&
|
||||
boundary === 'start' &&
|
||||
(phase === 'xtream.network.total' ||
|
||||
phase === 'xtream.cancel-session')
|
||||
) {
|
||||
const method =
|
||||
phase === 'xtream.network.total'
|
||||
? 'xtreamRequest'
|
||||
: 'xtreamCancelSession';
|
||||
call = [...calls.values()]
|
||||
.filter(
|
||||
(candidate) =>
|
||||
!candidate.ended &&
|
||||
!candidate.quarantined &&
|
||||
!candidate.downstreamMatched &&
|
||||
candidate.marker.method === method
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.receivedEpochMs - right.receivedEpochMs ||
|
||||
left.marker.ipcCallId - right.marker.ipcCallId
|
||||
)[0];
|
||||
if (call) {
|
||||
call.downstreamMatched = true;
|
||||
mainRequestCalls.set(requestId, call);
|
||||
} else {
|
||||
invalidReasons.push(
|
||||
'main-phase-preload-performance-marker-missing'
|
||||
);
|
||||
}
|
||||
}
|
||||
timeline.push({
|
||||
...(call
|
||||
? {
|
||||
action: call.marker.action,
|
||||
categoryType: call.marker.categoryType,
|
||||
contentType: call.marker.contentType,
|
||||
ipcCallId: call.marker.ipcCallId,
|
||||
}
|
||||
: {}),
|
||||
boundary,
|
||||
durationMs: durationMs as number | null,
|
||||
epochMs,
|
||||
phase,
|
||||
requestId,
|
||||
type: 'xtream-main-phase',
|
||||
});
|
||||
return true;
|
||||
},
|
||||
acceptPreload(senderWebContentsId, receivedEpochMs, input) {
|
||||
if (
|
||||
!active ||
|
||||
senderWebContentsId !== expectedWebContentsId ||
|
||||
!protocol.isEpoch(receivedEpochMs)
|
||||
) {
|
||||
return helpers.invalidate(
|
||||
'preload-marker-sender-or-epoch-invalid'
|
||||
);
|
||||
}
|
||||
if (receivedEpochMs < captureStartedEpochMs) {
|
||||
return helpers.invalidate(
|
||||
'preload-marker-before-capture-start'
|
||||
);
|
||||
}
|
||||
if (receivedEpochMs < lastReceivedEpochMs) {
|
||||
return helpers.invalidate(
|
||||
'preload-marker-sender-or-epoch-invalid'
|
||||
);
|
||||
}
|
||||
const marker = protocol.parseMarker(input);
|
||||
if (!marker) {
|
||||
return helpers.invalidate('preload-marker-invalid-schema');
|
||||
}
|
||||
if (marker.sourceEpochMs < captureStartedEpochMs) {
|
||||
return helpers.invalidate(
|
||||
'preload-marker-before-capture-start'
|
||||
);
|
||||
}
|
||||
if (
|
||||
marker.sourceEpochMs - receivedEpochMs >=
|
||||
preloadSourceReceiptSkewToleranceMs
|
||||
) {
|
||||
return helpers.invalidate(
|
||||
'preload-marker-source-after-receipt'
|
||||
);
|
||||
}
|
||||
const normalizedReceivedEpochMs = Math.max(
|
||||
receivedEpochMs,
|
||||
marker.sourceEpochMs
|
||||
);
|
||||
if (marker.sourceEpochMs < lastSourceEpochMs) {
|
||||
return helpers.invalidate(
|
||||
'preload-marker-source-epoch-regressed'
|
||||
);
|
||||
}
|
||||
const key = protocol.correlationKey(marker);
|
||||
if (!key) {
|
||||
return helpers.invalidate(
|
||||
'preload-marker-correlation-key-invalid'
|
||||
);
|
||||
}
|
||||
lastReceivedEpochMs = receivedEpochMs;
|
||||
lastSourceEpochMs = marker.sourceEpochMs;
|
||||
if (marker.boundary === 'start') {
|
||||
if (calls.has(marker.ipcCallId) || activeByKey.has(key)) {
|
||||
helpers.quarantine(key);
|
||||
return helpers.invalidate('preload-marker-duplicate-key');
|
||||
}
|
||||
const call: XtreamIpcCallState = {
|
||||
completionOutcome: null,
|
||||
downstreamMatched: false,
|
||||
ended: false,
|
||||
key,
|
||||
marker,
|
||||
quarantined: false,
|
||||
receivedEpochMs: normalizedReceivedEpochMs,
|
||||
};
|
||||
calls.set(marker.ipcCallId, call);
|
||||
activeByKey.set(key, call);
|
||||
const waiting = pendingRequests.get(key) ?? [];
|
||||
if (waiting.length === 1 && waiting[0]) {
|
||||
protocol.applyMarkerIdentity(waiting[0], marker);
|
||||
call.downstreamMatched = true;
|
||||
pendingRequests.delete(key);
|
||||
} else if (waiting.length > 1) {
|
||||
helpers.quarantine(key);
|
||||
return helpers.invalidate('preload-marker-duplicate-key');
|
||||
}
|
||||
} else {
|
||||
const call = calls.get(marker.ipcCallId);
|
||||
if (
|
||||
!call ||
|
||||
call.ended ||
|
||||
call.quarantined ||
|
||||
call.key !== key ||
|
||||
!protocol.sameMarkerIdentity(call.marker, marker)
|
||||
) {
|
||||
return helpers.invalidate('preload-marker-unmatched-end');
|
||||
}
|
||||
call.ended = true;
|
||||
call.completionOutcome = marker.outcome;
|
||||
activeByKey.delete(key);
|
||||
}
|
||||
timeline.push({
|
||||
action: marker.action,
|
||||
boundary: marker.boundary,
|
||||
categoryType: marker.categoryType,
|
||||
contentType: marker.contentType,
|
||||
epochMs: normalizedReceivedEpochMs,
|
||||
ipcCallId: marker.ipcCallId,
|
||||
itemCount: marker.itemCount,
|
||||
method: marker.method,
|
||||
operationId: marker.operationId,
|
||||
outcome: marker.outcome,
|
||||
playlistId: marker.playlistId,
|
||||
sessionId: marker.sessionId,
|
||||
sourceEpochMs: marker.sourceEpochMs,
|
||||
type: 'xtream-preload-marker',
|
||||
});
|
||||
return true;
|
||||
},
|
||||
matchDatabaseCancel(input) {
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return helpers.matchKey(protocol.readCancelKey(input));
|
||||
} catch {
|
||||
helpers.invalidate('preload-marker-correlation-input-invalid');
|
||||
return protocol.unavailableIdentity(
|
||||
'xtream-preload-performance-marker-ambiguous'
|
||||
);
|
||||
}
|
||||
},
|
||||
matchDatabaseRequest(input) {
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return helpers.matchKey(protocol.readRequestKey(input));
|
||||
} catch {
|
||||
helpers.invalidate('preload-marker-correlation-input-invalid');
|
||||
return protocol.unavailableIdentity(
|
||||
'xtream-preload-performance-marker-ambiguous'
|
||||
);
|
||||
}
|
||||
},
|
||||
start(webContentsId, startedEpochMs) {
|
||||
const validStart = protocol.isEpoch(startedEpochMs);
|
||||
active = true;
|
||||
captureStartedEpochMs = validStart
|
||||
? startedEpochMs
|
||||
: Number.POSITIVE_INFINITY;
|
||||
expectedWebContentsId = webContentsId;
|
||||
lastMainEpochMs = captureStartedEpochMs;
|
||||
lastReceivedEpochMs = captureStartedEpochMs;
|
||||
lastSourceEpochMs = captureStartedEpochMs;
|
||||
timeline = [];
|
||||
invalidReasons = [];
|
||||
if (!validStart) {
|
||||
invalidReasons.push('capture-start-epoch-invalid');
|
||||
}
|
||||
calls.clear();
|
||||
activeByKey.clear();
|
||||
pendingRequests.clear();
|
||||
quarantinedKeys.clear();
|
||||
mainPhases.clear();
|
||||
mainRequestCalls.clear();
|
||||
},
|
||||
stop() {
|
||||
for (const call of calls.values()) {
|
||||
if (!call.ended && !call.quarantined) {
|
||||
invalidReasons.push('preload-marker-unclosed');
|
||||
}
|
||||
if (
|
||||
call.ended &&
|
||||
(call.marker.method === 'xtreamRequest' ||
|
||||
call.marker.method === 'xtreamCancelSession')
|
||||
) {
|
||||
const linkedRequest = [...mainRequestCalls.entries()].find(
|
||||
([, candidate]) => candidate === call
|
||||
);
|
||||
if (!linkedRequest) {
|
||||
invalidReasons.push('preload-main-linkage-missing');
|
||||
continue;
|
||||
}
|
||||
const lifecycleInvalidReason = mainLifecycle.validate({
|
||||
method: call.marker.method,
|
||||
outcome: call.completionOutcome,
|
||||
phases: helpers.mainPhasesForRequest(linkedRequest[0]),
|
||||
});
|
||||
if (lifecycleInvalidReason) {
|
||||
invalidReasons.push(lifecycleInvalidReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const phase of mainPhases.values()) {
|
||||
if (phase.endEpochMs === null) {
|
||||
invalidReasons.push('main-phase-unclosed');
|
||||
}
|
||||
}
|
||||
for (const key of mainPhases.keys()) {
|
||||
const requestId = key.slice(0, key.indexOf('\u0000'));
|
||||
if (!mainRequestCalls.has(requestId)) {
|
||||
invalidReasons.push(
|
||||
'main-phase-preload-performance-marker-missing'
|
||||
);
|
||||
}
|
||||
}
|
||||
active = false;
|
||||
expectedWebContentsId = null;
|
||||
return Object.freeze({
|
||||
invalidReasons: Object.freeze([...invalidReasons]),
|
||||
quarantinedKeyCount: quarantinedKeys.size,
|
||||
timeline: Object.freeze([...timeline]),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import type { XtreamIpcMarkerCaptureOptions } from './xtream-ipc-marker-events.model';
|
||||
import type { XtreamIpcRequestIdentity } from './xtream-ipc-marker-events.model';
|
||||
|
||||
export interface SafeXtreamPreloadMarker {
|
||||
action: string | null;
|
||||
boundary: 'end' | 'start';
|
||||
categoryType: string | null;
|
||||
contentType: string | null;
|
||||
ipcCallId: number;
|
||||
itemCount: number | null;
|
||||
method: string;
|
||||
operationId: string | null;
|
||||
outcome: 'error' | 'success' | null;
|
||||
playlistId: string | null;
|
||||
schemaVersion: number;
|
||||
sessionId: string | null;
|
||||
sourceEpochMs: number;
|
||||
}
|
||||
|
||||
export interface SafeXtreamMainPhase {
|
||||
boundary: 'end' | 'start';
|
||||
durationMs: number | null;
|
||||
epochMs: number;
|
||||
phase: string;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface XtreamIpcMarkerProtocol {
|
||||
applyMarkerIdentity(
|
||||
identity: XtreamIpcRequestIdentity,
|
||||
marker: SafeXtreamPreloadMarker
|
||||
): void;
|
||||
correlationKey(marker: SafeXtreamPreloadMarker): string | null;
|
||||
isEpoch(value: unknown): value is number;
|
||||
parseMainPhase(input: unknown): SafeXtreamMainPhase | null;
|
||||
parseMarker(input: unknown): SafeXtreamPreloadMarker | null;
|
||||
readCancelKey(input: unknown): string | null | undefined;
|
||||
readRequestKey(input: unknown): string | null | undefined;
|
||||
sameMarkerIdentity(
|
||||
start: SafeXtreamPreloadMarker,
|
||||
end: SafeXtreamPreloadMarker
|
||||
): boolean;
|
||||
unavailableIdentity(reason: string): XtreamIpcRequestIdentity;
|
||||
}
|
||||
|
||||
export function createXtreamIpcMarkerProtocol(
|
||||
options: XtreamIpcMarkerCaptureOptions
|
||||
): XtreamIpcMarkerProtocol {
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
const markerKeys = new Set([
|
||||
'schemaVersion',
|
||||
'sourceEpochMs',
|
||||
'ipcCallId',
|
||||
'method',
|
||||
'boundary',
|
||||
'outcome',
|
||||
'playlistId',
|
||||
'operationId',
|
||||
'sessionId',
|
||||
'action',
|
||||
'categoryType',
|
||||
'contentType',
|
||||
'itemCount',
|
||||
]);
|
||||
const actions = new Set(options.actions);
|
||||
const mainPhases = new Set(options.mainPhases);
|
||||
const methods = new Set(options.methods);
|
||||
const categoryTypes = new Set(['live', 'movies', 'series']);
|
||||
const contentTypes = new Set(['live', 'movie', 'series']);
|
||||
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const rendererRequestId = /^xtream-[0-9a-z]+-[1-9][0-9]*$/;
|
||||
const internalRequestId =
|
||||
/^xtream-main-(?:fallback-[1-9][0-9]*|[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/;
|
||||
const operationToMethod: Readonly<Record<string, string>> = {
|
||||
DB_DELETE_PLAYLIST: 'dbDeletePlaylist',
|
||||
DB_DELETE_XTREAM_CONTENT: 'dbDeleteXtreamContent',
|
||||
DB_GET_CATEGORIES: 'dbGetCategories',
|
||||
DB_GET_CONTENT: 'dbGetContent',
|
||||
DB_GLOBAL_SEARCH: 'dbGlobalSearch',
|
||||
DB_RESTORE_XTREAM_USER_DATA: 'dbRestoreXtreamUserData',
|
||||
DB_SAVE_CATEGORIES: 'dbSaveCategories',
|
||||
DB_SAVE_CONTENT: 'dbSaveContent',
|
||||
DB_SEARCH_CONTENT: 'dbSearchContent',
|
||||
};
|
||||
const helpers = {
|
||||
correlationKey(marker: SafeXtreamPreloadMarker): string | null {
|
||||
const values: Array<number | string | null> = [marker.method];
|
||||
switch (marker.method) {
|
||||
case 'xtreamRequest':
|
||||
if (marker.action === null) return null;
|
||||
values.push(marker.ipcCallId);
|
||||
break;
|
||||
case 'xtreamCancelSession':
|
||||
values.push(marker.sessionId);
|
||||
break;
|
||||
case 'dbGetCategories':
|
||||
case 'dbSaveCategories':
|
||||
values.push(marker.playlistId, marker.categoryType);
|
||||
break;
|
||||
case 'dbGetContent':
|
||||
values.push(marker.playlistId, marker.contentType);
|
||||
break;
|
||||
case 'dbSaveContent':
|
||||
values.push(
|
||||
marker.playlistId,
|
||||
marker.contentType,
|
||||
marker.operationId
|
||||
);
|
||||
break;
|
||||
case 'dbDeletePlaylist':
|
||||
values.push(
|
||||
marker.playlistId,
|
||||
marker.operationId ?? '<no-operation-id>'
|
||||
);
|
||||
break;
|
||||
case 'dbDeleteXtreamContent':
|
||||
case 'dbRestoreXtreamUserData':
|
||||
values.push(marker.playlistId, marker.operationId);
|
||||
break;
|
||||
case 'dbSearchContent':
|
||||
values.push(marker.playlistId);
|
||||
break;
|
||||
case 'dbGlobalSearch':
|
||||
break;
|
||||
case 'dbCancelOperation':
|
||||
values.push(marker.operationId);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return values.some((value) => value === null)
|
||||
? null
|
||||
: JSON.stringify(values);
|
||||
},
|
||||
isEpoch(value: unknown): value is number {
|
||||
return (
|
||||
typeof value === 'number' && Number.isFinite(value) && value > 0
|
||||
);
|
||||
},
|
||||
isIdentifier(value: unknown): value is string {
|
||||
return typeof value === 'string' && identifierPattern.test(value);
|
||||
},
|
||||
isNullableIdentifier(value: unknown): value is string | null {
|
||||
return value === null || helpers.isIdentifier(value);
|
||||
},
|
||||
isNullableMember(
|
||||
value: unknown,
|
||||
allowed: ReadonlySet<string>
|
||||
): value is string | null {
|
||||
return (
|
||||
value === null ||
|
||||
(typeof value === 'string' && allowed.has(value))
|
||||
);
|
||||
},
|
||||
isRecord(value: unknown): value is JsonRecord {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value)
|
||||
);
|
||||
},
|
||||
};
|
||||
return {
|
||||
applyMarkerIdentity(identity, marker): void {
|
||||
identity.ipcCallId = marker.ipcCallId;
|
||||
identity.operationId = marker.operationId;
|
||||
identity.operationIdUnavailableReason = null;
|
||||
identity.sourceEpochMs = marker.sourceEpochMs;
|
||||
},
|
||||
correlationKey(marker): string | null {
|
||||
return helpers.correlationKey(marker);
|
||||
},
|
||||
isEpoch(value): value is number {
|
||||
return helpers.isEpoch(value);
|
||||
},
|
||||
parseMainPhase(input): SafeXtreamMainPhase | null {
|
||||
try {
|
||||
if (!helpers.isRecord(input)) {
|
||||
return null;
|
||||
}
|
||||
const boundary = input['boundary'];
|
||||
const durationMs = input['durationMs'];
|
||||
const epochMs = input['epochMs'];
|
||||
const phase = input['phase'];
|
||||
const requestId = input['requestId'];
|
||||
if (
|
||||
(boundary !== 'start' && boundary !== 'end') ||
|
||||
!helpers.isEpoch(epochMs) ||
|
||||
typeof phase !== 'string' ||
|
||||
!mainPhases.has(phase) ||
|
||||
typeof requestId !== 'string' ||
|
||||
requestId.length > 64 ||
|
||||
(!rendererRequestId.test(requestId) &&
|
||||
!internalRequestId.test(requestId)) ||
|
||||
(boundary === 'start'
|
||||
? durationMs !== null
|
||||
: typeof durationMs !== 'number' ||
|
||||
!Number.isFinite(durationMs) ||
|
||||
durationMs < 0)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
boundary,
|
||||
durationMs: durationMs as number | null,
|
||||
epochMs,
|
||||
phase,
|
||||
requestId,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
parseMarker(input): SafeXtreamPreloadMarker | null {
|
||||
try {
|
||||
if (
|
||||
!helpers.isRecord(input) ||
|
||||
Reflect.ownKeys(input).length !== markerKeys.size ||
|
||||
Reflect.ownKeys(input).some(
|
||||
(key) => typeof key !== 'string' || !markerKeys.has(key)
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const boundary = input['boundary'];
|
||||
const outcome = input['outcome'];
|
||||
const itemCount = input['itemCount'];
|
||||
if (
|
||||
input['schemaVersion'] !== options.schemaVersion ||
|
||||
!helpers.isEpoch(input['sourceEpochMs']) ||
|
||||
!Number.isSafeInteger(input['ipcCallId']) ||
|
||||
Number(input['ipcCallId']) < 1 ||
|
||||
typeof input['method'] !== 'string' ||
|
||||
!methods.has(input['method']) ||
|
||||
(boundary !== 'start' && boundary !== 'end') ||
|
||||
(boundary === 'start'
|
||||
? outcome !== null
|
||||
: outcome !== 'success' && outcome !== 'error') ||
|
||||
!helpers.isNullableIdentifier(input['playlistId']) ||
|
||||
!helpers.isNullableIdentifier(input['operationId']) ||
|
||||
!helpers.isNullableIdentifier(input['sessionId']) ||
|
||||
!helpers.isNullableMember(input['action'], actions) ||
|
||||
!helpers.isNullableMember(
|
||||
input['categoryType'],
|
||||
categoryTypes
|
||||
) ||
|
||||
!helpers.isNullableMember(
|
||||
input['contentType'],
|
||||
contentTypes
|
||||
) ||
|
||||
(itemCount !== null &&
|
||||
(!Number.isSafeInteger(itemCount) ||
|
||||
Number(itemCount) < 0 ||
|
||||
Number(itemCount) > 1_000_000))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return input as unknown as SafeXtreamPreloadMarker;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
readCancelKey(input): string | null | undefined {
|
||||
if (!helpers.isRecord(input) || input['type'] !== 'cancel') {
|
||||
return undefined;
|
||||
}
|
||||
return helpers.correlationKey({
|
||||
method: 'dbCancelOperation',
|
||||
operationId:
|
||||
typeof input['operationId'] === 'string'
|
||||
? input['operationId']
|
||||
: null,
|
||||
} as SafeXtreamPreloadMarker);
|
||||
},
|
||||
readRequestKey(input): string | null | undefined {
|
||||
if (
|
||||
!helpers.isRecord(input) ||
|
||||
typeof input['operation'] !== 'string'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const method = operationToMethod[input['operation']];
|
||||
if (!method) {
|
||||
return undefined;
|
||||
}
|
||||
const payload = helpers.isRecord(input['payload'])
|
||||
? input['payload']
|
||||
: {};
|
||||
return helpers.correlationKey({
|
||||
method,
|
||||
playlistId:
|
||||
typeof payload['playlistId'] === 'string'
|
||||
? payload['playlistId']
|
||||
: null,
|
||||
operationId:
|
||||
typeof payload['operationId'] === 'string'
|
||||
? payload['operationId']
|
||||
: null,
|
||||
categoryType:
|
||||
typeof payload['type'] === 'string' &&
|
||||
categoryTypes.has(payload['type'])
|
||||
? payload['type']
|
||||
: null,
|
||||
contentType:
|
||||
typeof payload['type'] === 'string' &&
|
||||
contentTypes.has(payload['type'])
|
||||
? payload['type']
|
||||
: null,
|
||||
} as SafeXtreamPreloadMarker);
|
||||
},
|
||||
sameMarkerIdentity(start, end): boolean {
|
||||
return (
|
||||
start.ipcCallId === end.ipcCallId &&
|
||||
start.method === end.method &&
|
||||
start.playlistId === end.playlistId &&
|
||||
start.operationId === end.operationId &&
|
||||
start.sessionId === end.sessionId &&
|
||||
start.action === end.action &&
|
||||
start.categoryType === end.categoryType &&
|
||||
start.contentType === end.contentType &&
|
||||
start.itemCount === end.itemCount
|
||||
);
|
||||
},
|
||||
unavailableIdentity(reason): XtreamIpcRequestIdentity {
|
||||
return {
|
||||
ipcCallId: null,
|
||||
operationId: null,
|
||||
operationIdUnavailableReason: reason,
|
||||
sourceEpochMs: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
XTREAM_WORKER_REQUEST_IDENTITY_KIND,
|
||||
xtreamWorkerRequestIdentityKind,
|
||||
} from './xtream-worker-request-identity';
|
||||
|
||||
export const XTREAM_IPC_PRODUCER_NAMESPACE = {
|
||||
LEGACY_PRELOAD: 'legacy-preload',
|
||||
XTREAM_PRELOAD: 'xtream-preload',
|
||||
} as const;
|
||||
|
||||
export type XtreamIpcProducerNamespace =
|
||||
(typeof XTREAM_IPC_PRODUCER_NAMESPACE)[keyof typeof XTREAM_IPC_PRODUCER_NAMESPACE];
|
||||
|
||||
export interface XtreamIpcProducerIdentity {
|
||||
readonly ipcCallId: number;
|
||||
readonly namespace: XtreamIpcProducerNamespace;
|
||||
readonly sourceEpochMs: number;
|
||||
}
|
||||
|
||||
const LEGACY_PRELOAD_METHODS = new Set<string>([
|
||||
'dbGetAppPlaylist',
|
||||
'dbUpsertAppPlaylist',
|
||||
]);
|
||||
const XTREAM_PRELOAD_METHODS = new Set<string>([
|
||||
'dbCancelOperation',
|
||||
'dbDeletePlaylist',
|
||||
'dbDeleteXtreamContent',
|
||||
'dbGetCategories',
|
||||
'dbGetContent',
|
||||
'dbGlobalSearch',
|
||||
'dbRestoreXtreamUserData',
|
||||
'dbSaveCategories',
|
||||
'dbSaveContent',
|
||||
'dbSearchContent',
|
||||
'xtreamCancelSession',
|
||||
'xtreamRequest',
|
||||
]);
|
||||
|
||||
export function xtreamIpcProducerNamespaceForMethod(
|
||||
method: string
|
||||
): XtreamIpcProducerNamespace | null {
|
||||
if (LEGACY_PRELOAD_METHODS.has(method)) {
|
||||
return XTREAM_IPC_PRODUCER_NAMESPACE.LEGACY_PRELOAD;
|
||||
}
|
||||
if (XTREAM_PRELOAD_METHODS.has(method)) {
|
||||
return XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function xtreamIpcProducerNamespaceForWorkerOperation(
|
||||
operation: string
|
||||
): XtreamIpcProducerNamespace | null {
|
||||
const kind = xtreamWorkerRequestIdentityKind(operation);
|
||||
if (kind === XTREAM_WORKER_REQUEST_IDENTITY_KIND.LEGACY_PRELOAD) {
|
||||
return XTREAM_IPC_PRODUCER_NAMESPACE.LEGACY_PRELOAD;
|
||||
}
|
||||
if (
|
||||
kind === XTREAM_WORKER_REQUEST_IDENTITY_KIND.PRODUCER_IPC ||
|
||||
kind === XTREAM_WORKER_REQUEST_IDENTITY_KIND.PRODUCER_OPERATION
|
||||
) {
|
||||
return XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function xtreamIpcProducerIdentityKey(
|
||||
namespace: XtreamIpcProducerNamespace,
|
||||
ipcCallId: number
|
||||
): string {
|
||||
return `${namespace}\0${ipcCallId}`;
|
||||
}
|
||||
|
||||
export function hasDistinctMonotonicXtreamIpcProducerIdentities(
|
||||
identities: readonly XtreamIpcProducerIdentity[]
|
||||
): boolean {
|
||||
const keys = identities.map(({ ipcCallId, namespace }) =>
|
||||
xtreamIpcProducerIdentityKey(namespace, ipcCallId)
|
||||
);
|
||||
if (new Set(keys).size !== keys.length) return false;
|
||||
for (const namespace of Object.values(XTREAM_IPC_PRODUCER_NAMESPACE)) {
|
||||
const ordered = identities
|
||||
.filter((identity) => identity.namespace === namespace)
|
||||
.sort((left, right) => left.ipcCallId - right.ipcCallId);
|
||||
if (
|
||||
ordered.some(
|
||||
({ sourceEpochMs }, index) =>
|
||||
index > 0 &&
|
||||
sourceEpochMs < Number(ordered[index - 1]?.sourceEpochMs)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { WorkerRequestPerformanceMetrics } from './m3u-refresh-cancellation-contract';
|
||||
import type { XtreamIpcAttributionSpan } from './xtream-phase-attribution';
|
||||
import {
|
||||
xtreamIpcProducerIdentityKey,
|
||||
xtreamIpcProducerNamespaceForMethod,
|
||||
xtreamIpcProducerNamespaceForWorkerOperation,
|
||||
} from './xtream-ipc-producer-identity';
|
||||
|
||||
const DB_METHOD_OPERATION = Object.freeze({
|
||||
dbDeletePlaylist: 'DB_DELETE_PLAYLIST',
|
||||
dbDeleteXtreamContent: 'DB_DELETE_XTREAM_CONTENT',
|
||||
dbGetAppPlaylist: 'DB_GET_APP_PLAYLIST',
|
||||
dbGetCategories: 'DB_GET_CATEGORIES',
|
||||
dbGetContent: 'DB_GET_CONTENT',
|
||||
dbRestoreXtreamUserData: 'DB_RESTORE_XTREAM_USER_DATA',
|
||||
dbSaveCategories: 'DB_SAVE_CATEGORIES',
|
||||
dbSaveContent: 'DB_SAVE_CONTENT',
|
||||
dbUpsertAppPlaylist: 'DB_UPSERT_APP_PLAYLIST',
|
||||
});
|
||||
const WORKER_OPERATIONS = new Set<string>(Object.values(DB_METHOD_OPERATION));
|
||||
|
||||
export function xtreamWorkerOperationForIpcMethod(
|
||||
method: string
|
||||
): string | null {
|
||||
return (
|
||||
DB_METHOD_OPERATION[method as keyof typeof DB_METHOD_OPERATION] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function assertXtreamIpcWorkerIdentityBinding(
|
||||
spans: readonly XtreamIpcAttributionSpan[],
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
): void {
|
||||
const workerByIdentity = new Map<string, WorkerRequestPerformanceMetrics>();
|
||||
for (const request of requests) {
|
||||
if (request.ipcCallId === null) continue;
|
||||
const namespace = xtreamIpcProducerNamespaceForWorkerOperation(
|
||||
typeof request.operation === 'string' ? request.operation : ''
|
||||
);
|
||||
if (namespace === null) invalid();
|
||||
const key = xtreamIpcProducerIdentityKey(namespace, request.ipcCallId);
|
||||
if (workerByIdentity.has(key)) invalid();
|
||||
workerByIdentity.set(key, request);
|
||||
}
|
||||
const boundWorkerIdentities = new Set<string>();
|
||||
for (const span of spans) {
|
||||
if (span.boundary !== 'request') continue;
|
||||
const operation = xtreamWorkerOperationForIpcMethod(span.method);
|
||||
if (operation === null) continue;
|
||||
const namespace = xtreamIpcProducerNamespaceForMethod(span.method);
|
||||
if (namespace === null) invalid();
|
||||
const key = xtreamIpcProducerIdentityKey(namespace, span.ipcCallId);
|
||||
const worker = workerByIdentity.get(key);
|
||||
if (
|
||||
!worker ||
|
||||
worker.operation !== operation ||
|
||||
worker.sourceEpochMs !== span.sourceEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
boundWorkerIdentities.add(key);
|
||||
}
|
||||
if (
|
||||
boundWorkerIdentities.size !== workerByIdentity.size ||
|
||||
[...workerByIdentity].some(
|
||||
([key, request]) =>
|
||||
!boundWorkerIdentities.has(key) ||
|
||||
typeof request.operation !== 'string' ||
|
||||
!WORKER_OPERATIONS.has(request.operation)
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid Xtream IPC worker identity binding');
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { WorkerRequestPerformanceMetrics } from './m3u-refresh-cancellation-contract';
|
||||
import type {
|
||||
XtreamIpcAttributionSpan,
|
||||
XtreamPhaseAttributionInput,
|
||||
} from './xtream-phase-attribution';
|
||||
import { xtreamWorkerOperationForIpcMethod } from './xtream-ipc-worker-identity-binding';
|
||||
|
||||
interface ProducerIdentity {
|
||||
readonly ipcCallId: number;
|
||||
readonly sourceEpochMs: number;
|
||||
}
|
||||
|
||||
export function bindFixtureIpcWorkerIdentities(
|
||||
input: XtreamPhaseAttributionInput,
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
): {
|
||||
readonly phaseCapture: XtreamPhaseAttributionInput;
|
||||
readonly requests: readonly WorkerRequestPerformanceMetrics[];
|
||||
} {
|
||||
const identities = new Map<
|
||||
WorkerRequestPerformanceMetrics,
|
||||
ProducerIdentity
|
||||
>();
|
||||
const methods = new Set(
|
||||
input.ipcSpans
|
||||
.filter(({ boundary }) => boundary === 'request')
|
||||
.map(({ method }) => method)
|
||||
.filter(
|
||||
(method) => xtreamWorkerOperationForIpcMethod(method) !== null
|
||||
)
|
||||
);
|
||||
for (const method of methods) {
|
||||
const operation = xtreamWorkerOperationForIpcMethod(method);
|
||||
if (operation === null) fixtureError();
|
||||
const calls = requestSpans(input.ipcSpans, method);
|
||||
const workers = requests
|
||||
.filter((request) => request.operation === operation)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
Number(left.requestReceivedEpochMs) -
|
||||
Number(right.requestReceivedEpochMs) ||
|
||||
String(left.requestId).localeCompare(
|
||||
String(right.requestId)
|
||||
)
|
||||
);
|
||||
if (calls.length !== workers.length) fixtureError();
|
||||
calls.forEach((call, index) => {
|
||||
const worker = workers[index];
|
||||
if (
|
||||
!worker ||
|
||||
call.outcome !== (worker.success ? 'success' : 'error') ||
|
||||
worker.requestReceivedEpochMs === null ||
|
||||
call.sourceEpochMs > worker.requestReceivedEpochMs
|
||||
) {
|
||||
fixtureError();
|
||||
}
|
||||
identities.set(worker, {
|
||||
ipcCallId: call.ipcCallId,
|
||||
sourceEpochMs: call.sourceEpochMs,
|
||||
});
|
||||
});
|
||||
}
|
||||
const bound = requests.map((request) => {
|
||||
const identity = identities.get(request);
|
||||
if (identity) return { ...request, ...identity };
|
||||
if (request.ipcCallId !== null || request.sourceEpochMs !== null) {
|
||||
fixtureError();
|
||||
}
|
||||
return request;
|
||||
});
|
||||
return { phaseCapture: input, requests: bound };
|
||||
}
|
||||
|
||||
function requestSpans(
|
||||
spans: readonly XtreamIpcAttributionSpan[],
|
||||
method: string
|
||||
): readonly XtreamIpcAttributionSpan[] {
|
||||
return spans
|
||||
.filter((span) => span.boundary === 'request' && span.method === method)
|
||||
.sort((left, right) => left.ipcCallId - right.ipcCallId);
|
||||
}
|
||||
|
||||
function fixtureError(): never {
|
||||
throw new Error('invalid Xtream IPC worker identity fixture');
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import { XTREAM_SCENARIO_ID } from './xtream-benchmark-contract';
|
||||
import { createXtreamAssemblerFixture } from './xtream-iteration-assembler.fixture';
|
||||
import { assembleXtreamRawIteration } from './xtream-iteration-assembler';
|
||||
import { summarizeXtreamUiActionSamples } from './xtream-ui-action-probe';
|
||||
|
||||
describe('Xtream raw iteration assembler background UI evidence', () => {
|
||||
it('binds samples to the full refresh lifecycle after the delete prelude', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI
|
||||
);
|
||||
const request = input.mainCapture.requests.find(
|
||||
({ operation }) => operation === 'DB_DELETE_XTREAM_CONTENT'
|
||||
);
|
||||
const background = input.terminal.backgroundProbe;
|
||||
const operation = input.terminal.operation;
|
||||
const storeTerminalEpochMs =
|
||||
input.rendererCapture.probe.storeTerminalEpochMs;
|
||||
assert.ok(request?.workStartedEpochMs);
|
||||
assert.ok(request.workEndedEpochMs);
|
||||
assert.ok(background);
|
||||
assert.ok(operation);
|
||||
assert.ok(storeTerminalEpochMs);
|
||||
const step = (storeTerminalEpochMs - request.workEndedEpochMs) / 12;
|
||||
assert.ok(step > 0);
|
||||
const samples = background.result.samples.map((sample, index) => {
|
||||
const startEpochMs =
|
||||
Number(request.workEndedEpochMs) + step * (index * 2 + 1);
|
||||
const endEpochMs = startEpochMs + step;
|
||||
return {
|
||||
...sample,
|
||||
endEpochMs,
|
||||
latencyMs: step,
|
||||
startEpochMs,
|
||||
};
|
||||
});
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
terminal: {
|
||||
...input.terminal,
|
||||
backgroundProbe: {
|
||||
...background,
|
||||
completedEpochMs: samples.at(-1)?.endEpochMs ?? 0,
|
||||
result: {
|
||||
...summary,
|
||||
dashboardPaintVisibility: 'visible-without-overlay',
|
||||
samples,
|
||||
},
|
||||
startedEpochMs: request.workStartedEpochMs,
|
||||
},
|
||||
operation: {
|
||||
...operation,
|
||||
terminalEpochMs: request.workEndedEpochMs,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts samples that finish before the delete prelude completes', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI
|
||||
);
|
||||
const request = input.mainCapture.requests.find(
|
||||
({ operation }) => operation === 'DB_DELETE_XTREAM_CONTENT'
|
||||
);
|
||||
const background = input.terminal.backgroundProbe;
|
||||
const operation = input.terminal.operation;
|
||||
assert.ok(request?.workStartedEpochMs);
|
||||
assert.ok(request.workEndedEpochMs);
|
||||
assert.ok(background);
|
||||
assert.ok(operation);
|
||||
const step =
|
||||
(request.workEndedEpochMs - background.startedEpochMs) / 12;
|
||||
assert.ok(step > 0);
|
||||
const samples = background.result.samples.map((sample, index) => {
|
||||
const startEpochMs =
|
||||
background.startedEpochMs + step * (index * 2 + 1);
|
||||
const endEpochMs = startEpochMs + step;
|
||||
return {
|
||||
...sample,
|
||||
endEpochMs,
|
||||
latencyMs: step,
|
||||
startEpochMs,
|
||||
};
|
||||
});
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
assert.ok(
|
||||
Number(samples.at(-1)?.endEpochMs) < request.workEndedEpochMs
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
terminal: {
|
||||
...input.terminal,
|
||||
backgroundProbe: {
|
||||
...background,
|
||||
completedEpochMs: samples.at(-1)?.endEpochMs ?? 0,
|
||||
result: {
|
||||
...summary,
|
||||
dashboardPaintVisibility: 'visible-without-overlay',
|
||||
samples,
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
...operation,
|
||||
terminalEpochMs: request.workEndedEpochMs,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects overlapping samples', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI
|
||||
);
|
||||
const background = input.terminal.backgroundProbe;
|
||||
assert.ok(background);
|
||||
const samples = background.result.samples.map((sample, index, all) => {
|
||||
if (index !== 1) return sample;
|
||||
const previous = all[0];
|
||||
assert.ok(previous);
|
||||
const startEpochMs = previous.endEpochMs - 0.1;
|
||||
return {
|
||||
...sample,
|
||||
latencyMs: sample.endEpochMs - startEpochMs,
|
||||
startEpochMs,
|
||||
};
|
||||
});
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
terminal: {
|
||||
...input.terminal,
|
||||
backgroundProbe: {
|
||||
...background,
|
||||
result: {
|
||||
...summary,
|
||||
dashboardPaintVisibility:
|
||||
'visible-without-overlay',
|
||||
samples,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
/xtream-iteration-assembly-invalid/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a sample after the final refresh store terminal', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.BACKGROUND_UI
|
||||
);
|
||||
const background = input.terminal.backgroundProbe;
|
||||
const storeTerminalEpochMs =
|
||||
input.rendererCapture.probe.storeTerminalEpochMs;
|
||||
assert.ok(background);
|
||||
assert.ok(storeTerminalEpochMs);
|
||||
const samples = background.result.samples.map((sample, index, all) => {
|
||||
if (index !== all.length - 1) return sample;
|
||||
const endEpochMs = storeTerminalEpochMs + 0.1;
|
||||
return {
|
||||
...sample,
|
||||
endEpochMs,
|
||||
latencyMs: endEpochMs - sample.startEpochMs,
|
||||
};
|
||||
});
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
terminal: {
|
||||
...input.terminal,
|
||||
backgroundProbe: {
|
||||
...background,
|
||||
completedEpochMs: samples.at(-1)?.endEpochMs ?? 0,
|
||||
result: {
|
||||
...summary,
|
||||
dashboardPaintVisibility:
|
||||
'visible-without-overlay',
|
||||
samples,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
/xtream-iteration-assembly-invalid/
|
||||
);
|
||||
});
|
||||
});
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
import type { RendererPerformancePhaseEvent } from '@iptvnator/shared/logging';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import type {
|
||||
MainTimelineRecord,
|
||||
WorkerRequestPerformanceMetrics,
|
||||
} from './m3u-refresh-cancellation-contract';
|
||||
import {
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE,
|
||||
xtreamIpcProducerNamespaceForWorkerOperation,
|
||||
} from './xtream-ipc-producer-identity';
|
||||
import type { XtreamRawIterationResult } from './xtream-summary-contract';
|
||||
import type {
|
||||
XtreamBackgroundProbeEvidence,
|
||||
XtreamScenarioTerminalEvidence,
|
||||
} from './xtream-scenario-driver';
|
||||
import {
|
||||
summarizeXtreamUiActionSamples,
|
||||
type XtreamUiActionProbeResult,
|
||||
type XtreamUiActionSample,
|
||||
} from './xtream-ui-action-probe';
|
||||
|
||||
const STORE_PREFIX = 'store.';
|
||||
|
||||
export function createLegacyTimeline(
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
): MainTimelineRecord[] {
|
||||
const legacy = requests.filter(
|
||||
(request) =>
|
||||
xtreamIpcProducerNamespaceForWorkerOperation(
|
||||
request.operation ?? ''
|
||||
) === XTREAM_IPC_PRODUCER_NAMESPACE.LEGACY_PRELOAD
|
||||
);
|
||||
return legacy.flatMap((request) => {
|
||||
const source = requireFixtureValue(request.sourceEpochMs);
|
||||
const received = requireFixtureValue(request.requestReceivedEpochMs);
|
||||
const response = request.responseEpochMs;
|
||||
const completion = response + 0.1;
|
||||
const shared = {
|
||||
ipcCallId: requireFixtureValue(request.ipcCallId),
|
||||
operation: requireFixtureValue(request.operation),
|
||||
playlistId: requireFixtureValue(request.playlistId),
|
||||
};
|
||||
return [
|
||||
{
|
||||
...shared,
|
||||
epochMs: (source + received) / 2,
|
||||
requestId: requireFixtureValue(request.requestId),
|
||||
sourceEpochMs: source,
|
||||
type: 'db-request',
|
||||
},
|
||||
{
|
||||
...shared,
|
||||
epochMs: response,
|
||||
requestId: requireFixtureValue(request.requestId),
|
||||
success: true,
|
||||
type: 'db-response',
|
||||
},
|
||||
{
|
||||
...shared,
|
||||
epochMs: completion,
|
||||
sourceEpochMs: completion,
|
||||
success: true,
|
||||
type: 'preload-performance-success',
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export function createRendererCapture(
|
||||
raw: XtreamRawIterationResult,
|
||||
background: XtreamUiActionProbeResult | undefined
|
||||
) {
|
||||
const storeEvents = raw.phaseCapture.events.filter(({ phase }) =>
|
||||
phase.startsWith(STORE_PREFIX)
|
||||
);
|
||||
const itemsByPhaseId = new Map(
|
||||
storeEvents
|
||||
.filter(({ boundary }) => boundary === 'end')
|
||||
.map(({ correlationId, itemCount }) => [correlationId, itemCount])
|
||||
);
|
||||
const performancePhaseEvents = storeEvents.map(
|
||||
(event): RendererPerformancePhaseEvent => {
|
||||
const itemCount = itemsByPhaseId.get(event.correlationId) ?? null;
|
||||
return {
|
||||
boundary: event.boundary,
|
||||
epochMs: event.epochMs,
|
||||
...(itemCount === null
|
||||
? {}
|
||||
: { metadata: { items: itemCount } }),
|
||||
monotonicMs: event.epochMs,
|
||||
...(event.boundary === 'end' ? { outcome: 'success' } : {}),
|
||||
phase: event.phase as RendererPerformancePhaseEvent['phase'],
|
||||
phaseId: event.correlationId,
|
||||
schemaVersion: 1,
|
||||
};
|
||||
}
|
||||
);
|
||||
const probe = {
|
||||
cancelEligibleProgressEpochMs:
|
||||
raw.cancellation.clickEpochMs === null
|
||||
? null
|
||||
: raw.cancellation.clickEpochMs - 0.1,
|
||||
cancellationClickEpochMs: raw.cancellation.clickEpochMs,
|
||||
dbCancellationTerminalEpochMs:
|
||||
raw.cancellation.authoritativeTerminalEpochMs,
|
||||
dbOperationEvents: [],
|
||||
domReadyEpochMs: raw.phaseCapture.uiPaintEpochMs - 0.1,
|
||||
frameGapsMs: [20, 45],
|
||||
heartbeatDelaysMs: [5, 12],
|
||||
invalidReason: null,
|
||||
longTasksMs: [30, 75],
|
||||
operationStartEpochMs: raw.phaseCapture.operationStartEpochMs,
|
||||
performancePhaseEvents,
|
||||
routeReadyEpochMs: raw.phaseCapture.operationStartEpochMs + 0.1,
|
||||
scenarioId: raw.scenarioId,
|
||||
storeTerminalEpochMs: Math.max(
|
||||
...storeEvents
|
||||
.filter(({ boundary }) => boundary === 'end')
|
||||
.map(({ epochMs }) => epochMs)
|
||||
),
|
||||
terminalEpochMs: raw.phaseCapture.terminalEpochMs,
|
||||
uiPaintedEpochMs: raw.phaseCapture.uiPaintEpochMs,
|
||||
};
|
||||
return {
|
||||
captureCutoffEpochMs: raw.phaseCapture.terminalEpochMs + 0.5,
|
||||
consoleErrorBreakdown:
|
||||
raw.scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
? {
|
||||
databaseSaveCancellation: 1,
|
||||
fetchContentCancellation: 1,
|
||||
initializationCancellation: 1,
|
||||
unexpected: 0,
|
||||
}
|
||||
: {
|
||||
databaseSaveCancellation: 0,
|
||||
fetchContentCancellation: 0,
|
||||
initializationCancellation: 0,
|
||||
unexpected: 0,
|
||||
},
|
||||
consoleErrorCount:
|
||||
raw.scenarioId === XTREAM_SCENARIO_ID.CANCEL_IMPORT ? 3 : 0,
|
||||
cpuProfilePath: null,
|
||||
frameGap: {
|
||||
count: 2,
|
||||
max: 45,
|
||||
mean: 32.5,
|
||||
median: 32.5,
|
||||
min: 20,
|
||||
p95: 43.75,
|
||||
p99: 44.75,
|
||||
},
|
||||
heapSnapshotPath: null,
|
||||
heartbeatDelay: {
|
||||
count: 2,
|
||||
max: 12,
|
||||
mean: 8.5,
|
||||
median: 8.5,
|
||||
min: 5,
|
||||
p95: 11.65,
|
||||
p99: 11.93,
|
||||
},
|
||||
longTask: {
|
||||
count: 2,
|
||||
max: 75,
|
||||
mean: 52.5,
|
||||
median: 52.5,
|
||||
min: 30,
|
||||
p95: 72.75,
|
||||
p99: 74.55,
|
||||
},
|
||||
measurementStartEpochMs: raw.phaseCapture.operationStartEpochMs,
|
||||
pageErrorCount: 0,
|
||||
peakHeapUsedBytes: raw.renderer.peakHeapUsedBytes,
|
||||
postGcHeapUsedBytes: raw.renderer.postGcHeapUsedBytes,
|
||||
probe,
|
||||
storeToPaintMs:
|
||||
raw.phaseCapture.uiPaintEpochMs - probe.storeTerminalEpochMs,
|
||||
targetId: raw.renderer.targetId,
|
||||
tracePath: null,
|
||||
...(background ? { background } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createBackgroundEvidence(
|
||||
raw: XtreamRawIterationResult,
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
): XtreamBackgroundProbeEvidence<XtreamUiActionProbeResult> | null {
|
||||
if (raw.scenarioId !== XTREAM_SCENARIO_ID.BACKGROUND_UI) return null;
|
||||
const request = requireFixtureValue(
|
||||
requests.find(
|
||||
({ operation }) => operation === 'DB_DELETE_XTREAM_CONTENT'
|
||||
)
|
||||
);
|
||||
const start = requireFixtureValue(request.workStartedEpochMs);
|
||||
const deleteEnd = requireFixtureValue(request.workEndedEpochMs);
|
||||
const refreshEnd = Math.max(
|
||||
...raw.phaseCapture.events
|
||||
.filter(
|
||||
({ boundary, phase }) =>
|
||||
boundary === 'end' &&
|
||||
phase === 'store.xtream-import-terminal'
|
||||
)
|
||||
.map(({ epochMs }) => epochMs)
|
||||
);
|
||||
const step = (refreshEnd - deleteEnd) / 12;
|
||||
const categories = [
|
||||
'action',
|
||||
'navigation',
|
||||
'navigation',
|
||||
'action',
|
||||
'search',
|
||||
] as const;
|
||||
const kinds = [
|
||||
'type-filter',
|
||||
'dashboard',
|
||||
'sources',
|
||||
'name-a-z',
|
||||
'global-search',
|
||||
] as const;
|
||||
const samples: XtreamUiActionSample[] = kinds.map((kind, index) => ({
|
||||
activeAfter: true,
|
||||
activeBefore: true,
|
||||
category: requireFixtureValue(categories[index]),
|
||||
endEpochMs: deleteEnd + step * (index * 2 + 2),
|
||||
kind,
|
||||
latencyMs: step,
|
||||
startEpochMs: deleteEnd + step * (index * 2 + 1),
|
||||
}));
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
return {
|
||||
completedEpochMs: requireFixtureValue(samples.at(-1)).endEpochMs,
|
||||
operation: 'delete-xtream-content',
|
||||
operationId: requireFixtureValue(request.operationId),
|
||||
playlistId: requireFixtureValue(request.playlistId),
|
||||
result: {
|
||||
...summary,
|
||||
dashboardPaintVisibility: 'visible-without-overlay',
|
||||
samples,
|
||||
},
|
||||
startedEpochMs: start,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTerminal(
|
||||
raw: XtreamRawIterationResult,
|
||||
background: XtreamBackgroundProbeEvidence<XtreamUiActionProbeResult> | null
|
||||
): XtreamScenarioTerminalEvidence {
|
||||
const cancellation =
|
||||
raw.cancellation.clickEpochMs === null
|
||||
? null
|
||||
: {
|
||||
armedEpochMs: raw.phaseCapture.operationStartEpochMs - 0.5,
|
||||
clickEpochMs: raw.cancellation.clickEpochMs,
|
||||
current: 6_000,
|
||||
operationId: requireFixtureValue(
|
||||
raw.cancellation.failedOperationId
|
||||
),
|
||||
progressEpochMs: raw.cancellation.clickEpochMs - 0.1,
|
||||
threshold: 6_000,
|
||||
total: 60_000,
|
||||
};
|
||||
const operation =
|
||||
background === null
|
||||
? null
|
||||
: {
|
||||
active: false,
|
||||
operation: 'delete-xtream-content' as const,
|
||||
operationId: background.operationId,
|
||||
playlistId: background.playlistId,
|
||||
startedEpochMs: background.startedEpochMs - 0.1,
|
||||
terminalEpochMs:
|
||||
requireFixtureValue(background.result.samples[0])
|
||||
.startEpochMs -
|
||||
requireFixtureValue(background.result.samples[0])
|
||||
.latencyMs,
|
||||
terminalStatus: 'completed' as const,
|
||||
};
|
||||
return {
|
||||
backgroundProbe: background,
|
||||
cancellation,
|
||||
operation,
|
||||
scenarioId: raw.scenarioId,
|
||||
terminalEpochMs: raw.phaseCapture.terminalEpochMs - 0.1,
|
||||
};
|
||||
}
|
||||
|
||||
export function operationOutcomes(
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const request of requests) {
|
||||
const outcome = request.success ? 'success' : 'error';
|
||||
const key = `${request.operation}\0${outcome}`;
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||
}
|
||||
return [...counts].map(([key, count]) => {
|
||||
const [operation, outcome] = key.split('\0');
|
||||
return {
|
||||
count,
|
||||
operation: requireFixtureValue(operation),
|
||||
outcome: outcome as 'error' | 'success',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function scenarioOrdinal(scenarioId: XtreamScenarioId): number {
|
||||
return Object.values(XTREAM_SCENARIO_ID).indexOf(scenarioId) + 1;
|
||||
}
|
||||
|
||||
function requireFixtureValue<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error('invalid Xtream assembler fixture');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
type XtreamIterationDefinition,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { iteration } from './xtream-benchmark-report.fixtures';
|
||||
import type {
|
||||
WorkerCaptureMetrics,
|
||||
WorkerRequestPerformanceMetrics,
|
||||
} from './m3u-refresh-cancellation-contract';
|
||||
import type {
|
||||
XtreamIpcAttributionSpan,
|
||||
XtreamPhaseAttributionInput,
|
||||
} from './xtream-phase-attribution';
|
||||
import { XTREAM_ATTRIBUTION_PHASE as PHASE } from './xtream-phase-inventory';
|
||||
import {
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE,
|
||||
xtreamIpcProducerNamespaceForMethod,
|
||||
xtreamIpcProducerNamespaceForWorkerOperation,
|
||||
} from './xtream-ipc-producer-identity';
|
||||
import type { XtreamIterationAssemblyInput } from './xtream-iteration-assembler';
|
||||
import {
|
||||
createBackgroundEvidence,
|
||||
createLegacyTimeline,
|
||||
createRendererCapture,
|
||||
createTerminal,
|
||||
operationOutcomes,
|
||||
scenarioOrdinal,
|
||||
} from './xtream-iteration-assembler-fixture-runtime';
|
||||
import type { XtreamIpcTimelineRecord } from './xtream-ipc-marker-events.model';
|
||||
import type { XtreamRawIterationResult } from './xtream-summary-contract';
|
||||
|
||||
const MAIN_PHASES = new Set<string>([
|
||||
PHASE.CANCEL_SESSION,
|
||||
PHASE.JSON_TRANSFORM,
|
||||
PHASE.NETWORK_TOTAL,
|
||||
PHASE.RESPONSE_READY,
|
||||
]);
|
||||
|
||||
export function createXtreamAssemblerFixture(
|
||||
scenarioId: XtreamScenarioId
|
||||
): XtreamIterationAssemblyInput {
|
||||
const definition: XtreamIterationDefinition = {
|
||||
eligibleForComparison: true,
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: 'run-01',
|
||||
scenarioId,
|
||||
};
|
||||
const raw = iteration(definition, scenarioOrdinal(scenarioId));
|
||||
const worker = createWorker(raw);
|
||||
const background = createBackgroundEvidence(raw, worker.requests);
|
||||
const captureStartedEpochMs = raw.phaseCapture.operationStartEpochMs - 1;
|
||||
const captureCutoffEpochMs = raw.phaseCapture.terminalEpochMs + 1;
|
||||
const rendererCapture = createRendererCapture(raw, background?.result);
|
||||
return {
|
||||
catalogVerification: {
|
||||
databaseStateValid: true,
|
||||
fixtureIdentityValid: true,
|
||||
providerCategoryCount:
|
||||
raw.catalogVerification.providerCategoryCount,
|
||||
providerItemCount: raw.catalogVerification.providerItemCount,
|
||||
verifiedAfterCapture: true,
|
||||
},
|
||||
definition,
|
||||
diagnosticArtifacts: null,
|
||||
mainCapture: {
|
||||
cancelTimeline:
|
||||
raw.cancellationEvidence?.timeline.map((entry) => ({
|
||||
...entry,
|
||||
})) ?? [],
|
||||
capture: {
|
||||
cpuProfilePath: null,
|
||||
cpuSystemMicros: raw.main.cpuSystemMicros,
|
||||
cpuUserMicros: raw.main.cpuUserMicros,
|
||||
eventLoopDelay: {
|
||||
maxMs: raw.main.eventLoopDelayMaxMs,
|
||||
p95Ms: raw.main.eventLoopDelayP95Ms,
|
||||
p99Ms: raw.main.eventLoopDelayP99Ms,
|
||||
},
|
||||
eventLoopUtilization: null,
|
||||
eventLoopUtilizationUnavailableReason:
|
||||
raw.main.eventLoopUtilizationUnavailableReason,
|
||||
heapSnapshotPath: null,
|
||||
memory: {
|
||||
peakHeapUsedBytes: raw.main.peakHeapUsedBytes,
|
||||
peakRssBytes: raw.main.peakRssBytes,
|
||||
postGcHeapUsedBytes: raw.main.postGcHeapUsedBytes,
|
||||
postGcRssBytes: raw.main.peakRssBytes - 1,
|
||||
},
|
||||
rendererWindow: {
|
||||
responsiveEvents: raw.main.responsiveEvents,
|
||||
rss: {
|
||||
identity: { creationTime: 1, pid: 42 },
|
||||
missingSampleCount: 0,
|
||||
peakRssBytes: raw.renderer.peakRssBytes,
|
||||
unavailableReason: null,
|
||||
validSampleCount: 2,
|
||||
},
|
||||
unresponsiveEvents: raw.main.unresponsiveEvents,
|
||||
windowIdentity: {
|
||||
browserWindowId: raw.main.browserWindowId,
|
||||
webContentsId: raw.main.webContentsId,
|
||||
},
|
||||
},
|
||||
rssScope:
|
||||
'electron-main-process-including-worker-threads-and-native-memory',
|
||||
timeline: createLegacyTimeline(worker.requests),
|
||||
workers: [worker],
|
||||
},
|
||||
captureCutoffEpochMs,
|
||||
captureGeneration: 1,
|
||||
captureStartedEpochMs,
|
||||
captureStoppedEpochMs: captureCutoffEpochMs + 1,
|
||||
databaseWorkerCount: 1,
|
||||
invalidReasons: {
|
||||
capture: [],
|
||||
databaseWorkerCancel: [],
|
||||
xtreamIpc: [],
|
||||
},
|
||||
ipcTimeline: createXtreamTimeline(raw),
|
||||
lateDatabaseRequestCount: 0,
|
||||
lateEventCount: 0,
|
||||
measurementStartedEpochMs:
|
||||
raw.phaseCapture.operationStartEpochMs - 0.5,
|
||||
operationOutcomes: operationOutcomes(worker.requests),
|
||||
playlistRefreshWorkerCount: 0,
|
||||
quarantinedIpcMarkerCount: 0,
|
||||
requests: worker.requests,
|
||||
rollover: null,
|
||||
workers: [worker],
|
||||
},
|
||||
process: {
|
||||
electronPid: 20_000 + scenarioOrdinal(scenarioId),
|
||||
freshProcessVerified: true,
|
||||
launchId: `launch-${scenarioOrdinal(scenarioId)}`,
|
||||
profileDirectorySha256: String(scenarioOrdinal(scenarioId)).repeat(
|
||||
64
|
||||
),
|
||||
},
|
||||
rendererCapture,
|
||||
terminal: createTerminal(raw, background),
|
||||
trigger: {
|
||||
scenarioId,
|
||||
triggerEpochMs: raw.phaseCapture.operationStartEpochMs + 0.1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createWorker(raw: XtreamRawIterationResult): WorkerCaptureMetrics {
|
||||
const metric = raw.databaseWorker;
|
||||
return {
|
||||
cancelPostedEpochMs: raw.cancellation.databaseDispatchEpochMs,
|
||||
cpuSystemMicros: metric.cpuSystemMicros,
|
||||
cpuUserMicros: metric.cpuUserMicros,
|
||||
eventLoopDelay: {
|
||||
maxMs: metric.eventLoopDelayMaxMs,
|
||||
p95Ms: metric.eventLoopDelayP95Ms,
|
||||
p99Ms: metric.eventLoopDelayP99Ms,
|
||||
},
|
||||
eventLoopDelayUnavailableReason: null,
|
||||
eventLoopUtilization: metric.eventLoopUtilization,
|
||||
externalMemorySampleCount: 2,
|
||||
heapUsedSampleCount: 2,
|
||||
kind: 'database.worker',
|
||||
operationId: null,
|
||||
ordinal: metric.ordinal,
|
||||
peakExternalBytes: metric.peakExternalBytes,
|
||||
peakExternalUnavailableReason: null,
|
||||
peakHeapUsedBytes: metric.peakHeapUsedBytes,
|
||||
peakHeapUsedUnavailableReason: null,
|
||||
playlistId: null,
|
||||
postGcHeapUnavailableReason: null,
|
||||
postGcHeapUsedBytes: metric.postGcHeapUsedBytes,
|
||||
profilePath: null,
|
||||
requests: metric.requests.evidence.map(cloneRequest),
|
||||
responseEpochMs: null,
|
||||
snapshotPath: null,
|
||||
terminatedEpochMs: null,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneRequest(
|
||||
request: WorkerRequestPerformanceMetrics
|
||||
): WorkerRequestPerformanceMetrics {
|
||||
return {
|
||||
...request,
|
||||
eventLoopDelay:
|
||||
request.eventLoopDelay === null
|
||||
? null
|
||||
: { ...request.eventLoopDelay },
|
||||
phaseEvents: request.phaseEvents.map((event) => ({
|
||||
...event,
|
||||
...(event.metadata ? { metadata: { ...event.metadata } } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function createXtreamTimeline(
|
||||
raw: XtreamRawIterationResult
|
||||
): XtreamIpcTimelineRecord[] {
|
||||
const main = raw.phaseCapture.events
|
||||
.filter(({ phase }) => MAIN_PHASES.has(phase))
|
||||
.map((event): XtreamIpcTimelineRecord => ({
|
||||
action: event.providerAction,
|
||||
boundary: event.boundary,
|
||||
categoryType: event.categoryType,
|
||||
contentType: event.contentType,
|
||||
durationMs: event.durationMs,
|
||||
epochMs: event.epochMs,
|
||||
phase: event.phase,
|
||||
requestId: event.correlationId,
|
||||
type: 'xtream-main-phase',
|
||||
}));
|
||||
const markers = xtreamCalls(raw).flatMap(({ request, response }) => [
|
||||
marker(raw, request, 'start'),
|
||||
marker(raw, response, 'end'),
|
||||
]);
|
||||
return [...main, ...markers].sort((left, right) => {
|
||||
return left.epochMs - right.epochMs;
|
||||
});
|
||||
}
|
||||
|
||||
function marker(
|
||||
raw: XtreamRawIterationResult,
|
||||
span: XtreamIpcAttributionSpan,
|
||||
boundary: 'end' | 'start'
|
||||
): XtreamIpcTimelineRecord {
|
||||
const semantic = markerSemantic(raw, span);
|
||||
return {
|
||||
action: semantic.providerAction,
|
||||
boundary,
|
||||
categoryType: semantic.categoryType,
|
||||
contentType: semantic.contentType,
|
||||
epochMs: span.endEpochMs,
|
||||
ipcCallId: span.ipcCallId,
|
||||
itemCount: null,
|
||||
method: span.method,
|
||||
operationId:
|
||||
span.method === 'dbCancelOperation'
|
||||
? raw.cancellation.failedOperationId
|
||||
: semantic.operationId,
|
||||
outcome: boundary === 'start' ? null : span.outcome,
|
||||
playlistId: semantic.playlistId,
|
||||
sessionId: span.method.startsWith('xtream') ? 'session-1' : null,
|
||||
sourceEpochMs:
|
||||
boundary === 'start' ? span.sourceEpochMs : span.startEpochMs,
|
||||
type: 'xtream-preload-marker',
|
||||
};
|
||||
}
|
||||
|
||||
function markerSemantic(
|
||||
raw: XtreamRawIterationResult,
|
||||
span: XtreamIpcAttributionSpan
|
||||
) {
|
||||
if (span.method === 'xtreamRequest') {
|
||||
const providers = providerGroups(raw.phaseCapture);
|
||||
const calls = xtreamCalls(raw).filter(
|
||||
({ request }) => request.method === 'xtreamRequest'
|
||||
);
|
||||
const index = calls.findIndex(
|
||||
({ request }) => request.ipcCallId === span.ipcCallId
|
||||
);
|
||||
const provider = providers[index]?.[0];
|
||||
return {
|
||||
categoryType: null,
|
||||
contentType: null,
|
||||
operationId: null,
|
||||
playlistId: null,
|
||||
providerAction: provider?.providerAction ?? null,
|
||||
};
|
||||
}
|
||||
const request = raw.databaseWorker.requests.evidence.find(
|
||||
(candidate) =>
|
||||
candidate.ipcCallId === span.ipcCallId &&
|
||||
xtreamIpcProducerNamespaceForWorkerOperation(
|
||||
candidate.operation ?? ''
|
||||
) === XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD
|
||||
);
|
||||
const event = raw.phaseCapture.events.find(
|
||||
(candidate) =>
|
||||
candidate.correlationId === request?.requestId &&
|
||||
candidate.boundary === 'start'
|
||||
);
|
||||
return {
|
||||
categoryType: event?.categoryType ?? null,
|
||||
contentType: event?.contentType ?? null,
|
||||
operationId: request?.operationId ?? null,
|
||||
playlistId: request?.playlistId ?? null,
|
||||
providerAction: null,
|
||||
};
|
||||
}
|
||||
|
||||
function xtreamCalls(raw: XtreamRawIterationResult) {
|
||||
return pairIpc(raw.phaseCapture.ipcSpans).filter(
|
||||
({ request }) =>
|
||||
xtreamIpcProducerNamespaceForMethod(request.method) ===
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD
|
||||
);
|
||||
}
|
||||
|
||||
function pairIpc(spans: readonly XtreamIpcAttributionSpan[]) {
|
||||
const groups = new Map<string, XtreamIpcAttributionSpan[]>();
|
||||
for (const span of spans) {
|
||||
groups.set(span.correlationId, [
|
||||
...(groups.get(span.correlationId) ?? []),
|
||||
span,
|
||||
]);
|
||||
}
|
||||
return [...groups.values()].map((group) => {
|
||||
const request = group.find(({ boundary }) => boundary === 'request');
|
||||
const response = group.find(({ boundary }) => boundary === 'response');
|
||||
if (!request || !response) {
|
||||
throw new Error('invalid Xtream assembler IPC fixture');
|
||||
}
|
||||
return { request, response };
|
||||
});
|
||||
}
|
||||
|
||||
function providerGroups(capture: XtreamPhaseAttributionInput) {
|
||||
const groups = new Map<string, typeof capture.events>();
|
||||
for (const event of capture.events.filter(
|
||||
({ phase }) => phase === PHASE.NETWORK_TOTAL
|
||||
)) {
|
||||
groups.set(event.correlationId, [
|
||||
...(groups.get(event.correlationId) ?? []),
|
||||
event,
|
||||
]);
|
||||
}
|
||||
return [...groups.values()].sort((left, right) => {
|
||||
const leftStart = left[0];
|
||||
const rightStart = right[0];
|
||||
if (!leftStart || !rightStart) {
|
||||
throw new Error('invalid Xtream assembler provider fixture');
|
||||
}
|
||||
return leftStart.epochMs - rightStart.epochMs;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { createXtreamAssemblerFixture } from './xtream-iteration-assembler.fixture';
|
||||
import { assembleXtreamRawIteration } from './xtream-iteration-assembler';
|
||||
import { assertXtreamIterationResult } from './xtream-iteration-validity';
|
||||
|
||||
describe('Xtream raw iteration assembler', () => {
|
||||
for (const scenarioId of Object.values(XTREAM_SCENARIO_ID)) {
|
||||
it(`assembles measured ${scenarioId} evidence without inventing fields`, () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
scenarioId as XtreamScenarioId
|
||||
);
|
||||
|
||||
const result = assembleXtreamRawIteration(input);
|
||||
|
||||
assert.doesNotThrow(() => assertXtreamIterationResult(result));
|
||||
assert.equal(
|
||||
result.processIdentity.captureGeneration,
|
||||
input.mainCapture.captureGeneration
|
||||
);
|
||||
assert.equal(
|
||||
result.databaseWorker.requests.evidence.length,
|
||||
input.mainCapture.requests.length
|
||||
);
|
||||
assert.equal(result.validity.databaseWorkerCount, 1);
|
||||
assert.equal(result.validity.playlistWorkerCount, 0);
|
||||
assert.equal(result.main.cpuMetricScope, 'electron-main-thread');
|
||||
assert.equal(
|
||||
result.databaseWorker.cpuMetricScope,
|
||||
'sum-valid-request-thread-cpu'
|
||||
);
|
||||
assert.equal(
|
||||
result.databaseWorker.cpuUserMicros,
|
||||
result.databaseWorker.requests.evidence.reduce(
|
||||
(total, request) =>
|
||||
total +
|
||||
(request.invalidReason === null
|
||||
? Number(request.threadCpuUserMicros)
|
||||
: 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
assert.equal(
|
||||
result.cancellationEvidence === null,
|
||||
scenarioId !== XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it('fails closed on capture invalidation instead of manufacturing a valid result', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
mainCapture: {
|
||||
...input.mainCapture,
|
||||
invalidReasons: {
|
||||
...input.mainCapture.invalidReasons,
|
||||
capture: ['database-worker-activity-after-cutoff'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
/xtream-iteration-assembly-invalid/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a coarse trigger clock from the same millisecond as renderer start', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
);
|
||||
const operationStartEpochMs =
|
||||
input.rendererCapture.probe.operationStartEpochMs + 0.05;
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
rendererCapture: {
|
||||
...input.rendererCapture,
|
||||
measurementStartEpochMs: operationStartEpochMs,
|
||||
probe: {
|
||||
...input.rendererCapture.probe,
|
||||
operationStartEpochMs,
|
||||
},
|
||||
},
|
||||
trigger: {
|
||||
...input.trigger,
|
||||
triggerEpochMs: Math.floor(operationStartEpochMs),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a coarse trigger clock from the millisecond before renderer start', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
);
|
||||
const operationStartEpochMs =
|
||||
input.rendererCapture.probe.operationStartEpochMs + 0.05;
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
rendererCapture: {
|
||||
...input.rendererCapture,
|
||||
probe: {
|
||||
...input.rendererCapture.probe,
|
||||
operationStartEpochMs,
|
||||
},
|
||||
},
|
||||
trigger: {
|
||||
...input.trigger,
|
||||
triggerEpochMs: Math.floor(operationStartEpochMs) - 1,
|
||||
},
|
||||
}),
|
||||
/xtream-iteration-assembly-invalid/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts a late delete driver observation after renderer and main capture cutoffs', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.DELETE_LARGE
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
terminal: {
|
||||
...input.terminal,
|
||||
terminalEpochMs:
|
||||
input.mainCapture.captureCutoffEpochMs + 100,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts renderer cancellation delivery after the authoritative database response', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
);
|
||||
const authoritativeTerminalEpochMs = input.mainCapture.requests.find(
|
||||
({ operation, success }) =>
|
||||
operation === 'DB_SAVE_CONTENT' && !success
|
||||
)?.responseEpochMs;
|
||||
assert.equal(typeof authoritativeTerminalEpochMs, 'number');
|
||||
const dbCancellationTerminalEpochMs =
|
||||
Number(authoritativeTerminalEpochMs) + 0.25;
|
||||
assert.ok(
|
||||
dbCancellationTerminalEpochMs <
|
||||
Number(input.rendererCapture.probe.uiPaintedEpochMs)
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
rendererCapture: {
|
||||
...input.rendererCapture,
|
||||
probe: {
|
||||
...input.rendererCapture.probe,
|
||||
dbCancellationTerminalEpochMs,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts main observing a network cancel response after the next renderer IPC starts', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
);
|
||||
const databaseCancelStart = input.mainCapture.ipcTimeline.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'start' && method === 'dbCancelOperation'
|
||||
);
|
||||
assert.ok(databaseCancelStart);
|
||||
assert.equal(typeof databaseCancelStart.sourceEpochMs, 'number');
|
||||
const rendererHandoffEpochMs = Number(
|
||||
databaseCancelStart.sourceEpochMs
|
||||
);
|
||||
const delayedMainObservationEpochMs = rendererHandoffEpochMs + 0.1;
|
||||
assert.ok(delayedMainObservationEpochMs < databaseCancelStart.epochMs);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
mainCapture: {
|
||||
...input.mainCapture,
|
||||
ipcTimeline: input.mainCapture.ipcTimeline.map((record) =>
|
||||
record.boundary === 'end' &&
|
||||
record.method === 'xtreamCancelSession'
|
||||
? {
|
||||
...record,
|
||||
epochMs: delayedMainObservationEpochMs,
|
||||
sourceEpochMs: rendererHandoffEpochMs,
|
||||
}
|
||||
: record
|
||||
),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts the non-authoritative cancel IPC returning after the database acknowledgement', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
);
|
||||
const authoritativeTerminalEpochMs = input.mainCapture.requests.find(
|
||||
({ operation, success }) =>
|
||||
operation === 'DB_SAVE_CONTENT' && !success
|
||||
)?.responseEpochMs;
|
||||
assert.equal(typeof authoritativeTerminalEpochMs, 'number');
|
||||
const responseSourceEpochMs =
|
||||
Number(authoritativeTerminalEpochMs) + 0.1;
|
||||
const responseObservedEpochMs = responseSourceEpochMs + 0.1;
|
||||
assert.ok(
|
||||
responseObservedEpochMs <
|
||||
Number(input.rendererCapture.probe.uiPaintedEpochMs)
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
mainCapture: {
|
||||
...input.mainCapture,
|
||||
ipcTimeline: input.mainCapture.ipcTimeline
|
||||
.map((record) =>
|
||||
record.boundary === 'end' &&
|
||||
record.method === 'dbCancelOperation'
|
||||
? {
|
||||
...record,
|
||||
epochMs: responseObservedEpochMs,
|
||||
sourceEpochMs: responseSourceEpochMs,
|
||||
}
|
||||
: record
|
||||
)
|
||||
.sort((left, right) => left.epochMs - right.epochMs),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('binds store-to-paint duration to the renderer terminal and paint epochs', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.CANCEL_IMPORT
|
||||
);
|
||||
|
||||
assert.doesNotThrow(() => assembleXtreamRawIteration(input));
|
||||
assert.throws(
|
||||
() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
rendererCapture: {
|
||||
...input.rendererCapture,
|
||||
storeToPaintMs:
|
||||
input.rendererCapture.storeToPaintMs + 0.1,
|
||||
},
|
||||
}),
|
||||
/xtream-iteration-assembly-invalid/
|
||||
);
|
||||
});
|
||||
|
||||
it('includes trailing delete database and IPC evidence after the visible paint', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.DELETE_LARGE
|
||||
);
|
||||
const rendererTerminalEpochMs =
|
||||
input.rendererCapture.probe.uiPaintedEpochMs;
|
||||
|
||||
const result = assembleXtreamRawIteration({
|
||||
...input,
|
||||
rendererCapture: {
|
||||
...input.rendererCapture,
|
||||
probe: {
|
||||
...input.rendererCapture.probe,
|
||||
terminalEpochMs: rendererTerminalEpochMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
result.phaseCapture.terminalEpochMs > rendererTerminalEpochMs
|
||||
);
|
||||
assert.equal(
|
||||
result.phases.totalMs,
|
||||
result.phaseCapture.terminalEpochMs -
|
||||
result.phaseCapture.operationStartEpochMs
|
||||
);
|
||||
});
|
||||
|
||||
it('binds diagnostic durations and worker ordinal only from validated artifact evidence', () => {
|
||||
const input = createXtreamAssemblerFixture(
|
||||
XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assembleXtreamRawIteration({
|
||||
...input,
|
||||
definition: {
|
||||
...input.definition,
|
||||
eligibleForComparison: false,
|
||||
kind: 'diagnostic',
|
||||
runId: 'diagnostic',
|
||||
},
|
||||
diagnosticArtifacts: {
|
||||
artifacts: [],
|
||||
directorySha256: '0'.repeat(64),
|
||||
profiles: {
|
||||
databaseWorker: {
|
||||
durationMicros: 1,
|
||||
ordinal: 1,
|
||||
},
|
||||
main: { durationMicros: 1 },
|
||||
renderer: { durationMicros: 1 },
|
||||
},
|
||||
} as never,
|
||||
}),
|
||||
/xtream-iteration-assembly-invalid/
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type { XtreamIterationDefinition } from './xtream-benchmark-contract';
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
XTREAM_SCENARIO_ID,
|
||||
} from './xtream-benchmark-contract';
|
||||
import type { XtreamCatalogVerification } from './xtream-catalog-database-verification';
|
||||
import {
|
||||
assertXtreamDiagnosticArtifactEvidence,
|
||||
type XtreamDiagnosticArtifactEvidence,
|
||||
} from './xtream-diagnostic-artifacts';
|
||||
import { freezeDeep } from './xtream-exact-schema';
|
||||
import {
|
||||
assertXtreamMainEvidenceGates,
|
||||
createXtreamDatabaseWorkerMetrics,
|
||||
createXtreamMainMetrics,
|
||||
createXtreamRendererMetrics,
|
||||
} from './xtream-iteration-assembly-metrics';
|
||||
import { deriveXtreamPhaseCapture } from './xtream-iteration-assembly-phases';
|
||||
import { assembleXtreamScenarioEvidence } from './xtream-iteration-assembly-scenarios';
|
||||
import { assertXtreamIterationEvidenceBinding } from './xtream-iteration-evidence-binding';
|
||||
import { assertXtreamIterationResult } from './xtream-iteration-validity';
|
||||
import type { XtreamMainCaptureResult } from './m3u-refresh-main-capture';
|
||||
import { attributeXtreamPhases } from './xtream-phase-attribution';
|
||||
import type { XtreamRendererCaptureMetrics } from './xtream-renderer-capture';
|
||||
import { validXtreamRendererConsoleErrorEvidence } from './xtream-renderer-console-errors';
|
||||
import { assertCompleteXtreamRendererProbe } from './xtream-renderer-probe';
|
||||
import {
|
||||
XTREAM_SCENARIO_DATABASE_REQUEST_COUNT,
|
||||
type XtreamScenarioTerminalEvidence,
|
||||
type XtreamScenarioTriggerEvidence,
|
||||
} from './xtream-scenario-driver';
|
||||
import {
|
||||
XTREAM_NOT_APPLICABLE,
|
||||
type XtreamCatalogVerification as XtreamSummaryCatalogVerification,
|
||||
type XtreamIterationProcessIdentity,
|
||||
type XtreamRawIterationResult,
|
||||
} from './xtream-summary-contract';
|
||||
|
||||
export interface XtreamIterationProcessEvidence {
|
||||
readonly electronPid: number;
|
||||
readonly freshProcessVerified: true;
|
||||
readonly launchId: string;
|
||||
readonly profileDirectorySha256: string;
|
||||
}
|
||||
|
||||
export interface XtreamIterationAssemblyInput {
|
||||
readonly catalogVerification: XtreamCatalogVerification;
|
||||
readonly definition: XtreamIterationDefinition;
|
||||
readonly diagnosticArtifacts: XtreamDiagnosticArtifactEvidence | null;
|
||||
readonly mainCapture: XtreamMainCaptureResult;
|
||||
readonly process: XtreamIterationProcessEvidence;
|
||||
readonly rendererCapture: XtreamRendererCaptureMetrics;
|
||||
readonly terminal: XtreamScenarioTerminalEvidence;
|
||||
readonly trigger: XtreamScenarioTriggerEvidence;
|
||||
}
|
||||
|
||||
export function assembleXtreamRawIteration(
|
||||
input: XtreamIterationAssemblyInput
|
||||
): XtreamRawIterationResult {
|
||||
try {
|
||||
return assembleStrict(input);
|
||||
} catch {
|
||||
throw new Error('xtream-iteration-assembly-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function assembleStrict(
|
||||
input: XtreamIterationAssemblyInput
|
||||
): XtreamRawIterationResult {
|
||||
assertInputBoundary(input);
|
||||
const diagnostic = validateDiagnosticEvidence(input);
|
||||
assertXtreamMainEvidenceGates(input.mainCapture);
|
||||
assertCompleteXtreamRendererProbe(input.rendererCapture.probe);
|
||||
const phaseCapture = deriveXtreamPhaseCapture(
|
||||
input.definition.scenarioId,
|
||||
input.mainCapture,
|
||||
input.rendererCapture
|
||||
);
|
||||
const phases = attributeXtreamPhases(phaseCapture);
|
||||
const scenario = assembleXtreamScenarioEvidence({
|
||||
main: input.mainCapture,
|
||||
phaseCapture,
|
||||
renderer: input.rendererCapture,
|
||||
scenarioId: input.definition.scenarioId,
|
||||
terminal: input.terminal,
|
||||
trigger: input.trigger,
|
||||
});
|
||||
const context = {
|
||||
diagnostic,
|
||||
kind: input.definition.kind,
|
||||
};
|
||||
const raw: XtreamRawIterationResult = {
|
||||
...input.definition,
|
||||
cancellation: scenario.cancellation,
|
||||
cancellationEvidence: scenario.cancellationEvidence,
|
||||
catalogVerification: copyCatalogVerification(input.catalogVerification),
|
||||
databaseWorker: createXtreamDatabaseWorkerMetrics(
|
||||
input.mainCapture,
|
||||
context
|
||||
),
|
||||
main: createXtreamMainMetrics(input.mainCapture, context),
|
||||
phaseCapture,
|
||||
phases,
|
||||
playlistRefreshWorker: {
|
||||
instantiated: false,
|
||||
reason: XTREAM_NOT_APPLICABLE.PLAYLIST_REFRESH_WORKER,
|
||||
},
|
||||
processIdentity: createProcessIdentity(input),
|
||||
renderer: createXtreamRendererMetrics(
|
||||
input.mainCapture,
|
||||
input.rendererCapture,
|
||||
context,
|
||||
scenario.backgroundMetrics
|
||||
),
|
||||
validity: {
|
||||
actionContractValid: true,
|
||||
catalogContractValid: true,
|
||||
databaseWorkerCount: input.mainCapture.databaseWorkerCount,
|
||||
dbPhaseContractValid: true,
|
||||
diagnosticArtifactsValid:
|
||||
input.definition.kind === XTREAM_ITERATION_KIND.DIAGNOSTIC
|
||||
? true
|
||||
: null,
|
||||
lateDatabaseRequestCount:
|
||||
input.mainCapture.lateDatabaseRequestCount,
|
||||
playlistWorkerCount: input.mainCapture.playlistRefreshWorkerCount,
|
||||
rendererTargetCount: 1,
|
||||
},
|
||||
};
|
||||
assertXtreamIterationEvidenceBinding(raw);
|
||||
assertXtreamIterationResult(raw);
|
||||
return freezeDeep(raw);
|
||||
}
|
||||
|
||||
function assertInputBoundary(input: XtreamIterationAssemblyInput): void {
|
||||
const scenarioId = input.definition.scenarioId;
|
||||
if (
|
||||
!Object.values(XTREAM_SCENARIO_ID).includes(scenarioId) ||
|
||||
input.mainCapture.requests.length !==
|
||||
XTREAM_SCENARIO_DATABASE_REQUEST_COUNT[scenarioId] ||
|
||||
!validXtreamRendererConsoleErrorEvidence(
|
||||
input.rendererCapture.consoleErrorCount,
|
||||
input.rendererCapture.consoleErrorBreakdown,
|
||||
scenarioId
|
||||
) ||
|
||||
input.rendererCapture.pageErrorCount !== 0 ||
|
||||
input.rendererCapture.storeToPaintMs !==
|
||||
requireEpoch(input.rendererCapture.probe.uiPaintedEpochMs) -
|
||||
requireEpoch(input.rendererCapture.probe.storeTerminalEpochMs)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const catalog = input.catalogVerification;
|
||||
if (
|
||||
catalog.databaseStateValid !== true ||
|
||||
catalog.fixtureIdentityValid !== true ||
|
||||
catalog.verifiedAfterCapture !== true ||
|
||||
catalog.providerCategoryCount !== 100 ||
|
||||
catalog.providerItemCount !== 100_000
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function copyCatalogVerification(
|
||||
catalog: XtreamCatalogVerification
|
||||
): XtreamSummaryCatalogVerification {
|
||||
return {
|
||||
databaseStateValid: catalog.databaseStateValid,
|
||||
fixtureIdentityValid: catalog.fixtureIdentityValid,
|
||||
providerCategoryCount: catalog.providerCategoryCount as 100,
|
||||
providerItemCount: catalog.providerItemCount as 100_000,
|
||||
verifiedAfterCapture: catalog.verifiedAfterCapture,
|
||||
};
|
||||
}
|
||||
|
||||
function validateDiagnosticEvidence(
|
||||
input: XtreamIterationAssemblyInput
|
||||
): XtreamDiagnosticArtifactEvidence | null {
|
||||
if (input.definition.kind !== XTREAM_ITERATION_KIND.DIAGNOSTIC) {
|
||||
if (
|
||||
input.diagnosticArtifacts !== null ||
|
||||
input.mainCapture.capture.cpuProfilePath !== null ||
|
||||
input.mainCapture.capture.heapSnapshotPath !== null ||
|
||||
input.rendererCapture.cpuProfilePath !== null ||
|
||||
input.rendererCapture.heapSnapshotPath !== null ||
|
||||
input.rendererCapture.tracePath !== null ||
|
||||
input.mainCapture.workers.some(
|
||||
({ profilePath, snapshotPath }) =>
|
||||
profilePath !== null || snapshotPath !== null
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
assertXtreamDiagnosticArtifactEvidence(input.diagnosticArtifacts);
|
||||
if (
|
||||
input.process.profileDirectorySha256 !==
|
||||
input.diagnosticArtifacts.directorySha256
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return input.diagnosticArtifacts;
|
||||
}
|
||||
|
||||
function createProcessIdentity(
|
||||
input: XtreamIterationAssemblyInput
|
||||
): XtreamIterationProcessIdentity {
|
||||
const process = input.process;
|
||||
if (
|
||||
process.freshProcessVerified !== true ||
|
||||
!Number.isSafeInteger(process.electronPid) ||
|
||||
process.electronPid <= 0 ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(process.launchId) ||
|
||||
!/^[a-f0-9]{64}$/.test(process.profileDirectorySha256)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const captureGeneration = input.mainCapture.captureGeneration;
|
||||
if (!Number.isSafeInteger(captureGeneration) || captureGeneration <= 0) {
|
||||
invalid();
|
||||
}
|
||||
const identity = {
|
||||
captureGeneration,
|
||||
electronPid: process.electronPid,
|
||||
launchId: process.launchId,
|
||||
profileDirectorySha256: process.profileDirectorySha256,
|
||||
rendererTargetId: input.rendererCapture.targetId,
|
||||
browserWindowId:
|
||||
input.mainCapture.capture.rendererWindow.windowIdentity
|
||||
.browserWindowId,
|
||||
webContentsId:
|
||||
input.mainCapture.capture.rendererWindow.windowIdentity
|
||||
.webContentsId,
|
||||
};
|
||||
return {
|
||||
captureGeneration,
|
||||
electronPid: process.electronPid,
|
||||
freshProcess: true,
|
||||
generationIdentitySha256: createHash('sha256')
|
||||
.update(JSON.stringify(identity))
|
||||
.digest('hex'),
|
||||
launchId: process.launchId,
|
||||
profileDirectorySha256: process.profileDirectorySha256,
|
||||
};
|
||||
}
|
||||
|
||||
function requireEpoch(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)
|
||||
invalid();
|
||||
return value;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid Xtream iteration assembly input');
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
import type { WorkerRequestPerformanceMetrics } from './m3u-refresh-cancellation-contract';
|
||||
import type {
|
||||
XtreamMainCaptureResult,
|
||||
XtreamMainCaptureOperationOutcome,
|
||||
} from './m3u-refresh-main-capture';
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
type XtreamIterationKind,
|
||||
} from './xtream-benchmark-contract';
|
||||
import type { XtreamDiagnosticArtifactEvidence } from './xtream-diagnostic-evidence';
|
||||
import type { XtreamRendererCaptureMetrics } from './xtream-renderer-capture';
|
||||
import {
|
||||
XTREAM_NOT_APPLICABLE,
|
||||
XTREAM_MAIN_CPU_METRIC_SCOPE,
|
||||
XTREAM_WORKER_CPU_METRIC_SCOPE,
|
||||
XTREAM_WORKER_REQUEST_INVALID_REASON,
|
||||
XTREAM_WORKER_REQUEST_METRIC_SCOPE,
|
||||
XTREAM_WORKER_WHOLE_METRIC_SCOPE,
|
||||
type XtreamDatabaseWorkerMetrics,
|
||||
type XtreamMainMetrics,
|
||||
type XtreamRendererMetrics,
|
||||
type XtreamWorkerRequestAccounting,
|
||||
} from './xtream-summary-contract';
|
||||
import { deriveXtreamWorkerRequestWindowProxy } from './xtream-worker-request-proxy';
|
||||
|
||||
interface MetricContext {
|
||||
readonly diagnostic: XtreamDiagnosticArtifactEvidence | null;
|
||||
readonly kind: XtreamIterationKind;
|
||||
}
|
||||
|
||||
export function createXtreamMainMetrics(
|
||||
main: XtreamMainCaptureResult,
|
||||
context: MetricContext
|
||||
): XtreamMainMetrics {
|
||||
const memory = main.capture.memory;
|
||||
const window = main.capture.rendererWindow;
|
||||
return {
|
||||
...profile(context, 'main'),
|
||||
browserWindowId: window.windowIdentity.browserWindowId,
|
||||
cpuMetricScope: XTREAM_MAIN_CPU_METRIC_SCOPE.ELECTRON_MAIN_THREAD,
|
||||
cpuSystemMicros: main.capture.cpuSystemMicros,
|
||||
cpuUserMicros: main.capture.cpuUserMicros,
|
||||
eventLoopDelayMaxMs: main.capture.eventLoopDelay.maxMs,
|
||||
eventLoopDelayP95Ms: main.capture.eventLoopDelay.p95Ms,
|
||||
eventLoopDelayP99Ms: main.capture.eventLoopDelay.p99Ms,
|
||||
eventLoopUtilization: null,
|
||||
eventLoopUtilizationUnavailableReason:
|
||||
XTREAM_NOT_APPLICABLE.MAIN_EVENT_LOOP_UTILIZATION,
|
||||
peakHeapUsedBytes: memory.peakHeapUsedBytes,
|
||||
peakRssBytes: memory.peakRssBytes,
|
||||
postGcHeapUsedBytes: requireNumber(memory.postGcHeapUsedBytes),
|
||||
responsiveEvents: window.responsiveEvents,
|
||||
unresponsiveEvents: window.unresponsiveEvents,
|
||||
webContentsId: window.windowIdentity.webContentsId,
|
||||
};
|
||||
}
|
||||
|
||||
export function createXtreamRendererMetrics(
|
||||
main: XtreamMainCaptureResult,
|
||||
renderer: XtreamRendererCaptureMetrics,
|
||||
context: MetricContext,
|
||||
background: {
|
||||
readonly actionLatencyMs: number;
|
||||
readonly dashboardPaintVisibility: 'visible-without-overlay';
|
||||
readonly navigationLatencyMs: number;
|
||||
readonly searchLatencyMs: number;
|
||||
} | null
|
||||
): XtreamRendererMetrics {
|
||||
const rss = main.capture.rendererWindow.rss;
|
||||
if (
|
||||
rss.unavailableReason !== null ||
|
||||
rss.missingSampleCount !== 0 ||
|
||||
rss.validSampleCount <= 0
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return {
|
||||
...profile(context, 'renderer'),
|
||||
actionLatencyMs: background?.actionLatencyMs ?? null,
|
||||
consoleErrors: [],
|
||||
dashboardPaintVisibility: background?.dashboardPaintVisibility ?? null,
|
||||
frameGapMaxMs: requireDistribution(renderer.frameGap, 'max', true),
|
||||
frameGapP95Ms: requireDistribution(renderer.frameGap, 'p95', true),
|
||||
heartbeatMaxMs: requireDistribution(
|
||||
renderer.heartbeatDelay,
|
||||
'max',
|
||||
true
|
||||
),
|
||||
heartbeatP95Ms: requireDistribution(
|
||||
renderer.heartbeatDelay,
|
||||
'p95',
|
||||
true
|
||||
),
|
||||
longTaskCount: renderer.longTask.count,
|
||||
longTaskMaxMs:
|
||||
renderer.longTask.count === 0
|
||||
? 0
|
||||
: requireDistribution(renderer.longTask, 'max', true),
|
||||
navigationLatencyMs: background?.navigationLatencyMs ?? null,
|
||||
peakHeapUsedBytes: renderer.peakHeapUsedBytes,
|
||||
peakRssBytes: requireNumber(rss.peakRssBytes),
|
||||
postGcHeapUsedBytes: renderer.postGcHeapUsedBytes,
|
||||
searchLatencyMs: background?.searchLatencyMs ?? null,
|
||||
storeToPaintMs: renderer.storeToPaintMs,
|
||||
targetId: renderer.targetId,
|
||||
};
|
||||
}
|
||||
|
||||
export function createXtreamDatabaseWorkerMetrics(
|
||||
main: XtreamMainCaptureResult,
|
||||
context: MetricContext
|
||||
): XtreamDatabaseWorkerMetrics {
|
||||
const worker = main.workers[0];
|
||||
if (
|
||||
main.workers.length !== 1 ||
|
||||
!worker ||
|
||||
worker.kind !== 'database.worker' ||
|
||||
worker.heapUsedSampleCount <= 0 ||
|
||||
worker.externalMemorySampleCount <= 0 ||
|
||||
worker.peakHeapUsedUnavailableReason !== null ||
|
||||
worker.peakExternalUnavailableReason !== null ||
|
||||
worker.postGcHeapUnavailableReason !== null
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const requests = createRequestAccounting(worker.requests);
|
||||
const threadCpu = sumXtreamRequestThreadCpu(requests.evidence);
|
||||
if (
|
||||
context.diagnostic &&
|
||||
context.diagnostic.profiles.databaseWorker.ordinal !== worker.ordinal
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return {
|
||||
...profile(context, 'databaseWorker'),
|
||||
cpuMetricScope: XTREAM_WORKER_CPU_METRIC_SCOPE.SUM_VALID_REQUEST_WORK,
|
||||
cpuSystemMicros: threadCpu.system,
|
||||
cpuUserMicros: threadCpu.user,
|
||||
currentForCapture: true,
|
||||
eventLoopDelayMaxMs: requests.eventLoopDelayMaxMs,
|
||||
eventLoopDelayMetricScope:
|
||||
XTREAM_WORKER_REQUEST_METRIC_SCOPE.VALID_REQUEST_WINDOW_PROXY,
|
||||
eventLoopDelayP95Ms: requests.eventLoopDelayP95Ms,
|
||||
eventLoopDelayP99Ms: requests.eventLoopDelayP99Ms,
|
||||
eventLoopUtilization: requireNumber(worker.eventLoopUtilization),
|
||||
kind: 'database.worker',
|
||||
ordinal: worker.ordinal,
|
||||
peakExternalBytes: requireNumber(worker.peakExternalBytes),
|
||||
peakHeapUsedBytes: requireNumber(worker.peakHeapUsedBytes),
|
||||
postGcHeapUsedBytes: requireNumber(worker.postGcHeapUsedBytes),
|
||||
requests,
|
||||
rssBytes: null,
|
||||
rssUnavailableReason: XTREAM_NOT_APPLICABLE.DATABASE_WORKER_RSS,
|
||||
wholeWorkerMetricScope:
|
||||
XTREAM_WORKER_WHOLE_METRIC_SCOPE.OPERATION_WINDOW,
|
||||
};
|
||||
}
|
||||
|
||||
function sumXtreamRequestThreadCpu(
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
): { readonly system: number; readonly user: number } {
|
||||
let system = 0;
|
||||
let user = 0;
|
||||
for (const request of requests) {
|
||||
if (
|
||||
request.invalidReason ===
|
||||
XTREAM_WORKER_REQUEST_INVALID_REASON.OVERLAPPING_DATABASE_REQUESTS
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
request.invalidReason !== null ||
|
||||
request.threadCpuUnavailableReason !== null ||
|
||||
request.threadCpuSystemMicros === null ||
|
||||
request.threadCpuUserMicros === null
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
system += request.threadCpuSystemMicros;
|
||||
user += request.threadCpuUserMicros;
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(system) ||
|
||||
system < 0 ||
|
||||
!Number.isSafeInteger(user) ||
|
||||
user < 0
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return Object.freeze({ system, user });
|
||||
}
|
||||
|
||||
export function assertXtreamMainEvidenceGates(
|
||||
main: XtreamMainCaptureResult
|
||||
): void {
|
||||
if (
|
||||
main.databaseWorkerCount !== 1 ||
|
||||
main.playlistRefreshWorkerCount !== 0 ||
|
||||
main.workers.length !== 1 ||
|
||||
main.workers[0]?.kind !== 'database.worker' ||
|
||||
main.lateDatabaseRequestCount !== 0 ||
|
||||
main.lateEventCount !== 0 ||
|
||||
main.quarantinedIpcMarkerCount !== 0 ||
|
||||
main.rollover !== null ||
|
||||
Object.values(main.invalidReasons).some(
|
||||
(reasons) => reasons.length !== 0
|
||||
) ||
|
||||
main.capture.workers.length !== main.workers.length ||
|
||||
main.capture.workers[0]?.ordinal !== main.workers[0]?.ordinal ||
|
||||
main.requests.length !== main.workers[0]?.requests.length ||
|
||||
!sameOutcomes(main.operationOutcomes, main.requests)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function createRequestAccounting(
|
||||
evidence: readonly WorkerRequestPerformanceMetrics[]
|
||||
): XtreamWorkerRequestAccounting {
|
||||
const snapshot = evidence.map(cloneRequest);
|
||||
const proxy = deriveXtreamWorkerRequestWindowProxy(snapshot);
|
||||
if (!proxy) invalid();
|
||||
const invalidSampleCount = snapshot.filter(
|
||||
({ invalidReason }) =>
|
||||
invalidReason ===
|
||||
XTREAM_WORKER_REQUEST_INVALID_REASON.OVERLAPPING_DATABASE_REQUESTS
|
||||
).length;
|
||||
return {
|
||||
evidence: Object.freeze(snapshot),
|
||||
...proxy,
|
||||
invalidReasons:
|
||||
invalidSampleCount === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
count: invalidSampleCount,
|
||||
reason: XTREAM_WORKER_REQUEST_INVALID_REASON.OVERLAPPING_DATABASE_REQUESTS,
|
||||
},
|
||||
],
|
||||
invalidSampleCount,
|
||||
metricScope:
|
||||
XTREAM_WORKER_REQUEST_METRIC_SCOPE.VALID_REQUEST_WINDOW_PROXY,
|
||||
totalCount: snapshot.length,
|
||||
validSampleCount: snapshot.length - invalidSampleCount,
|
||||
};
|
||||
}
|
||||
|
||||
function cloneRequest(
|
||||
request: WorkerRequestPerformanceMetrics
|
||||
): WorkerRequestPerformanceMetrics {
|
||||
return {
|
||||
...request,
|
||||
eventLoopDelay:
|
||||
request.eventLoopDelay === null
|
||||
? null
|
||||
: { ...request.eventLoopDelay },
|
||||
phaseEvents: request.phaseEvents.map((event) => ({
|
||||
...event,
|
||||
...(event.metadata ? { metadata: { ...event.metadata } } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function sameOutcomes(
|
||||
actual: readonly XtreamMainCaptureOperationOutcome[],
|
||||
requests: readonly WorkerRequestPerformanceMetrics[]
|
||||
): boolean {
|
||||
const expected = new Map<string, number>();
|
||||
for (const request of requests) {
|
||||
const key = `${request.operation}\0${
|
||||
request.success ? 'success' : 'error'
|
||||
}`;
|
||||
expected.set(key, (expected.get(key) ?? 0) + 1);
|
||||
}
|
||||
const observed = new Map(
|
||||
actual.map(({ count, operation, outcome }) => [
|
||||
`${operation}\0${outcome}`,
|
||||
count,
|
||||
])
|
||||
);
|
||||
return (
|
||||
actual.every(({ outcome }) => outcome !== 'pending') &&
|
||||
expected.size === observed.size &&
|
||||
[...expected].every(([key, count]) => observed.get(key) === count)
|
||||
);
|
||||
}
|
||||
|
||||
function profile(
|
||||
context: MetricContext,
|
||||
process: 'databaseWorker' | 'main' | 'renderer'
|
||||
) {
|
||||
if (context.kind !== XTREAM_ITERATION_KIND.DIAGNOSTIC) {
|
||||
if (context.diagnostic !== null) invalid();
|
||||
return {
|
||||
cpuProfileDurationMicros: null,
|
||||
cpuProfileUnavailableReason:
|
||||
XTREAM_NOT_APPLICABLE.PROFILE_OUTSIDE_DIAGNOSTIC,
|
||||
} as const;
|
||||
}
|
||||
if (!context.diagnostic) invalid();
|
||||
const duration =
|
||||
process === 'databaseWorker'
|
||||
? context.diagnostic.profiles.databaseWorker.durationMicros
|
||||
: context.diagnostic.profiles[process].durationMicros;
|
||||
return {
|
||||
cpuProfileDurationMicros: duration,
|
||||
cpuProfileUnavailableReason: null,
|
||||
} as const;
|
||||
}
|
||||
|
||||
function requireDistribution(
|
||||
value: {
|
||||
readonly count: number;
|
||||
readonly max: number | null;
|
||||
readonly p95: number | null;
|
||||
},
|
||||
key: 'max' | 'p95',
|
||||
requireSamples: boolean
|
||||
): number {
|
||||
if (requireSamples && value.count <= 0) invalid();
|
||||
return requireNumber(value[key]);
|
||||
}
|
||||
|
||||
function requireNumber(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
||||
invalid();
|
||||
return value;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid Xtream assembly metrics evidence');
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
import { pairRendererPerformancePhaseEvents } from './renderer-performance-phase-events';
|
||||
import type { WorkerRequestPerformanceMetrics } from './m3u-refresh-cancellation-contract';
|
||||
import type { XtreamMainCaptureResult } from './m3u-refresh-main-capture';
|
||||
import type {
|
||||
XtreamAttributionPhaseEvent,
|
||||
XtreamIpcAttributionSpan,
|
||||
XtreamPhaseAttributionInput,
|
||||
} from './xtream-phase-attribution';
|
||||
import {
|
||||
XTREAM_ATTRIBUTION_PHASE as PHASE,
|
||||
XTREAM_PHASE_SEMANTIC_TYPE as SEMANTIC,
|
||||
type XtreamPhaseSemanticType,
|
||||
} from './xtream-phase-inventory';
|
||||
import {
|
||||
semanticTypeFromXtreamPhaseSource,
|
||||
xtreamDataSemanticSource,
|
||||
type XtreamPhaseSemanticSource,
|
||||
} from './xtream-phase-semantic-source';
|
||||
import {
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE,
|
||||
xtreamIpcProducerIdentityKey,
|
||||
xtreamIpcProducerNamespaceForWorkerOperation,
|
||||
} from './xtream-ipc-producer-identity';
|
||||
import { xtreamWorkerOperationForIpcMethod } from './xtream-ipc-worker-identity-binding';
|
||||
import type { XtreamIpcTimelineRecord } from './xtream-ipc-marker-events.model';
|
||||
import type { XtreamRendererCaptureMetrics } from './xtream-renderer-capture';
|
||||
|
||||
const MAIN_PHASES = new Set<string>([
|
||||
PHASE.CANCEL_SESSION,
|
||||
PHASE.JSON_TRANSFORM,
|
||||
PHASE.NETWORK_TOTAL,
|
||||
PHASE.RESPONSE_READY,
|
||||
]);
|
||||
const LEGACY_METHODS = Object.freeze({
|
||||
DB_GET_APP_PLAYLIST: 'dbGetAppPlaylist',
|
||||
DB_UPSERT_APP_PLAYLIST: 'dbUpsertAppPlaylist',
|
||||
});
|
||||
|
||||
export function deriveXtreamPhaseCapture(
|
||||
scenarioId: XtreamPhaseAttributionInput['scenarioId'],
|
||||
main: XtreamMainCaptureResult,
|
||||
renderer: XtreamRendererCaptureMetrics
|
||||
): XtreamPhaseAttributionInput {
|
||||
const operationStartEpochMs = renderer.probe.operationStartEpochMs;
|
||||
const rendererTerminalEpochMs = requireEpoch(
|
||||
renderer.probe.terminalEpochMs
|
||||
);
|
||||
const uiPaintEpochMs = requireEpoch(renderer.probe.uiPaintedEpochMs);
|
||||
const events = [
|
||||
...mainPhaseEvents(main),
|
||||
...workerPhaseEvents(main.requests, main),
|
||||
...rendererPhaseEvents(renderer),
|
||||
];
|
||||
const ipc = ipcSpans(main);
|
||||
const terminalEpochMs = Math.max(
|
||||
rendererTerminalEpochMs,
|
||||
...events.map(({ epochMs }) => epochMs),
|
||||
...ipc.map(({ endEpochMs }) => endEpochMs)
|
||||
);
|
||||
return Object.freeze({
|
||||
backgroundSearchMarkerObserved: events.some(
|
||||
({ phase }) => phase === PHASE.STORE_SEARCH_RESULTS
|
||||
),
|
||||
events: Object.freeze(events),
|
||||
ipcSpans: Object.freeze(ipc),
|
||||
operationStartEpochMs,
|
||||
scenarioId,
|
||||
terminalEpochMs,
|
||||
uiPaintEpochMs,
|
||||
});
|
||||
}
|
||||
|
||||
function mainPhaseEvents(
|
||||
main: XtreamMainCaptureResult
|
||||
): XtreamAttributionPhaseEvent[] {
|
||||
return main.ipcTimeline
|
||||
.filter(
|
||||
({ phase, type }) =>
|
||||
type === 'xtream-main-phase' &&
|
||||
typeof phase === 'string' &&
|
||||
MAIN_PHASES.has(phase)
|
||||
)
|
||||
.map((record) => {
|
||||
const source = {
|
||||
categoryType: record.categoryType ?? null,
|
||||
contentType: record.contentType ?? null,
|
||||
providerAction: record.action ?? null,
|
||||
} as XtreamPhaseSemanticSource;
|
||||
return phaseEvent({
|
||||
boundary: record.boundary,
|
||||
correlationId: requireText(record.requestId),
|
||||
durationMs: record.durationMs ?? null,
|
||||
epochMs: record.epochMs,
|
||||
itemCount: null,
|
||||
phase: requireText(record.phase),
|
||||
source,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function workerPhaseEvents(
|
||||
requests: readonly WorkerRequestPerformanceMetrics[],
|
||||
main: XtreamMainCaptureResult
|
||||
): XtreamAttributionPhaseEvent[] {
|
||||
return requests.flatMap((request) => {
|
||||
const correlationId = requireText(request.requestId);
|
||||
const source = workerSemanticSource(request, main);
|
||||
return request.phaseEvents.map((event) =>
|
||||
phaseEvent({
|
||||
boundary: event.boundary,
|
||||
correlationId,
|
||||
durationMs: event.durationMs,
|
||||
epochMs: event.epochMs,
|
||||
itemCount:
|
||||
event.boundary === 'end'
|
||||
? (event.metadata?.itemCount ?? null)
|
||||
: null,
|
||||
phase: event.phase,
|
||||
source,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function workerSemanticSource(
|
||||
request: WorkerRequestPerformanceMetrics,
|
||||
main: XtreamMainCaptureResult
|
||||
): XtreamPhaseSemanticSource {
|
||||
const operation = requireText(request.operation);
|
||||
const namespace = xtreamIpcProducerNamespaceForWorkerOperation(operation);
|
||||
if (namespace !== XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD) {
|
||||
return xtreamDataSemanticSource(null);
|
||||
}
|
||||
const marker = main.ipcTimeline.filter(
|
||||
(record) =>
|
||||
record.type === 'xtream-preload-marker' &&
|
||||
record.boundary === 'start' &&
|
||||
record.ipcCallId === request.ipcCallId &&
|
||||
record.sourceEpochMs === request.sourceEpochMs &&
|
||||
xtreamWorkerOperationForIpcMethod(record.method ?? '') === operation
|
||||
);
|
||||
if (marker.length !== 1 || !marker[0]) invalid();
|
||||
return {
|
||||
categoryType: marker[0].categoryType ?? null,
|
||||
contentType: marker[0].contentType ?? null,
|
||||
providerAction: null,
|
||||
} as XtreamPhaseSemanticSource;
|
||||
}
|
||||
|
||||
function rendererPhaseEvents(
|
||||
renderer: XtreamRendererCaptureMetrics
|
||||
): XtreamAttributionPhaseEvent[] {
|
||||
return pairRendererPerformancePhaseEvents(
|
||||
renderer.probe.performancePhaseEvents
|
||||
).flatMap((span) => {
|
||||
const source = rendererSemanticSource(span.phase);
|
||||
const itemCount = span.metadata?.items ?? null;
|
||||
return [
|
||||
phaseEvent({
|
||||
boundary: 'start',
|
||||
correlationId: span.phaseId,
|
||||
durationMs: null,
|
||||
epochMs: span.startedEpochMs,
|
||||
itemCount: null,
|
||||
phase: span.phase,
|
||||
source,
|
||||
}),
|
||||
phaseEvent({
|
||||
boundary: 'end',
|
||||
correlationId: span.phaseId,
|
||||
durationMs: span.durationMs,
|
||||
epochMs: span.endedEpochMs,
|
||||
itemCount,
|
||||
phase: span.phase,
|
||||
source,
|
||||
}),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function rendererSemanticSource(phase: string): XtreamPhaseSemanticSource {
|
||||
const semantic: XtreamPhaseSemanticType | null =
|
||||
phase === PHASE.STORE_PUBLISH_LIVE
|
||||
? SEMANTIC.LIVE_CONTENT
|
||||
: phase === PHASE.STORE_PUBLISH_VOD
|
||||
? SEMANTIC.VOD_CONTENT
|
||||
: phase === PHASE.STORE_PUBLISH_SERIES
|
||||
? SEMANTIC.SERIES_CONTENT
|
||||
: null;
|
||||
return xtreamDataSemanticSource(semantic);
|
||||
}
|
||||
|
||||
function ipcSpans(main: XtreamMainCaptureResult): XtreamIpcAttributionSpan[] {
|
||||
const spans = xtreamIpcSpans(main);
|
||||
for (const request of main.requests) {
|
||||
const operation = request.operation ?? '';
|
||||
const method = LEGACY_METHODS[operation as keyof typeof LEGACY_METHODS];
|
||||
if (method) spans.push(...legacyIpcPair(main, request, method));
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
function xtreamIpcSpans(
|
||||
main: XtreamMainCaptureResult
|
||||
): XtreamIpcAttributionSpan[] {
|
||||
const groups = new Map<string, typeof main.ipcTimeline>();
|
||||
for (const marker of main.ipcTimeline) {
|
||||
if (
|
||||
marker.type !== 'xtream-preload-marker' ||
|
||||
!Number.isSafeInteger(marker.ipcCallId) ||
|
||||
typeof marker.method !== 'string'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const key = xtreamIpcProducerIdentityKey(
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD,
|
||||
Number(marker.ipcCallId)
|
||||
);
|
||||
groups.set(key, [...(groups.get(key) ?? []), marker]);
|
||||
}
|
||||
return [...groups.entries()].flatMap(([correlationId, markers]) => {
|
||||
const start = markers.find(({ boundary }) => boundary === 'start');
|
||||
const end = markers.find(({ boundary }) => boundary === 'end');
|
||||
if (
|
||||
markers.length !== 2 ||
|
||||
!start ||
|
||||
!end ||
|
||||
start.method !== end.method ||
|
||||
start.outcome !== null ||
|
||||
!['error', 'success'].includes(String(end.outcome))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const sourceEpochMs = requireEpoch(start.sourceEpochMs);
|
||||
return [
|
||||
ipcSpan(
|
||||
'request',
|
||||
correlationId,
|
||||
start,
|
||||
sourceEpochMs,
|
||||
sourceEpochMs,
|
||||
end.outcome as 'error' | 'success'
|
||||
),
|
||||
ipcSpan(
|
||||
'response',
|
||||
correlationId,
|
||||
end,
|
||||
requireEpoch(end.sourceEpochMs),
|
||||
sourceEpochMs,
|
||||
end.outcome as 'error' | 'success'
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function legacyIpcPair(
|
||||
main: XtreamMainCaptureResult,
|
||||
request: WorkerRequestPerformanceMetrics,
|
||||
method: string
|
||||
): XtreamIpcAttributionSpan[] {
|
||||
const ipcCallId = requirePositiveInteger(request.ipcCallId);
|
||||
const sourceEpochMs = requireEpoch(request.sourceEpochMs);
|
||||
const requestTimeline = oneTimeline(main, request, 'db-request');
|
||||
const success = oneTimeline(main, request, 'preload-performance-success');
|
||||
const responseStartEpochMs = requireEpoch(request.responseEpochMs);
|
||||
const correlationId = xtreamIpcProducerIdentityKey(
|
||||
XTREAM_IPC_PRODUCER_NAMESPACE.LEGACY_PRELOAD,
|
||||
ipcCallId
|
||||
);
|
||||
return [
|
||||
{
|
||||
boundary: 'request',
|
||||
correlationId,
|
||||
endEpochMs: requestTimeline.epochMs,
|
||||
ipcCallId,
|
||||
method,
|
||||
outcome: 'success',
|
||||
sourceEpochMs,
|
||||
startEpochMs: sourceEpochMs,
|
||||
},
|
||||
{
|
||||
boundary: 'response',
|
||||
correlationId,
|
||||
endEpochMs: requireEpoch(success.sourceEpochMs),
|
||||
ipcCallId,
|
||||
method,
|
||||
outcome: 'success',
|
||||
sourceEpochMs,
|
||||
startEpochMs: responseStartEpochMs,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function oneTimeline(
|
||||
main: XtreamMainCaptureResult,
|
||||
request: WorkerRequestPerformanceMetrics,
|
||||
type: 'db-request' | 'preload-performance-success'
|
||||
) {
|
||||
const matches = main.capture.timeline.filter(
|
||||
(record) =>
|
||||
record.type === type &&
|
||||
record.ipcCallId === request.ipcCallId &&
|
||||
record.operation === request.operation &&
|
||||
record.playlistId === request.playlistId &&
|
||||
(type !== 'db-request' ||
|
||||
(record.requestId === request.requestId &&
|
||||
record.sourceEpochMs === request.sourceEpochMs))
|
||||
);
|
||||
if (matches.length !== 1 || !matches[0]) invalid();
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
function ipcSpan(
|
||||
boundary: 'request' | 'response',
|
||||
correlationId: string,
|
||||
marker: XtreamIpcTimelineRecord,
|
||||
startEpochMs: number,
|
||||
sourceEpochMs: number,
|
||||
outcome: 'error' | 'success'
|
||||
): XtreamIpcAttributionSpan {
|
||||
return {
|
||||
boundary,
|
||||
correlationId,
|
||||
endEpochMs: marker.epochMs,
|
||||
ipcCallId: requirePositiveInteger(marker.ipcCallId),
|
||||
method: requireText(marker.method),
|
||||
outcome,
|
||||
sourceEpochMs,
|
||||
startEpochMs,
|
||||
};
|
||||
}
|
||||
|
||||
function phaseEvent(input: {
|
||||
boundary: 'end' | 'start';
|
||||
correlationId: string;
|
||||
durationMs: number | null;
|
||||
epochMs: number;
|
||||
itemCount: number | null;
|
||||
phase: string;
|
||||
source: XtreamPhaseSemanticSource;
|
||||
}): XtreamAttributionPhaseEvent {
|
||||
const semanticType = semanticTypeFromXtreamPhaseSource(input.source);
|
||||
if (semanticType === undefined) invalid();
|
||||
return {
|
||||
boundary: input.boundary,
|
||||
categoryType: input.source.categoryType,
|
||||
contentType: input.source.contentType,
|
||||
correlationId: input.correlationId,
|
||||
durationMs: input.durationMs,
|
||||
epochMs: input.epochMs,
|
||||
itemCount: input.itemCount,
|
||||
phase: input.phase,
|
||||
providerAction: input.source.providerAction,
|
||||
semanticType,
|
||||
};
|
||||
}
|
||||
|
||||
function requireEpoch(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
||||
invalid();
|
||||
return value;
|
||||
}
|
||||
|
||||
function requirePositiveInteger(value: unknown): number {
|
||||
if (!Number.isSafeInteger(value) || Number(value) <= 0) invalid();
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
function requireText(value: unknown): string {
|
||||
if (typeof value !== 'string' || value.length === 0) invalid();
|
||||
return value;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid Xtream assembly phase evidence');
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
|
||||
import {
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import type { XtreamMainCaptureResult } from './m3u-refresh-main-capture';
|
||||
import type {
|
||||
XtreamIpcAttributionSpan,
|
||||
XtreamPhaseAttributionInput,
|
||||
} from './xtream-phase-attribution';
|
||||
import type { XtreamRendererCaptureMetrics } from './xtream-renderer-capture';
|
||||
import type {
|
||||
XtreamScenarioTerminalEvidence,
|
||||
XtreamScenarioTriggerEvidence,
|
||||
} from './xtream-scenario-driver';
|
||||
import {
|
||||
XTREAM_CANCEL_NETWORK_ABORT_REASON,
|
||||
type XtreamCancellationEvidence,
|
||||
type XtreamCancellationMetrics,
|
||||
} from './xtream-summary-contract';
|
||||
import {
|
||||
summarizeXtreamUiActionSamples,
|
||||
XTREAM_UI_ACTION_ORDER,
|
||||
type XtreamUiActionProbeResult,
|
||||
type XtreamUiActionSample,
|
||||
} from './xtream-ui-action-probe';
|
||||
|
||||
export interface XtreamScenarioAssembly {
|
||||
readonly backgroundMetrics: {
|
||||
readonly actionLatencyMs: number;
|
||||
readonly dashboardPaintVisibility: 'visible-without-overlay';
|
||||
readonly navigationLatencyMs: number;
|
||||
readonly searchLatencyMs: number;
|
||||
} | null;
|
||||
readonly cancellation: XtreamCancellationMetrics;
|
||||
readonly cancellationEvidence: XtreamCancellationEvidence | null;
|
||||
}
|
||||
|
||||
export function assembleXtreamScenarioEvidence(input: {
|
||||
readonly main: XtreamMainCaptureResult;
|
||||
readonly phaseCapture: XtreamPhaseAttributionInput;
|
||||
readonly renderer: XtreamRendererCaptureMetrics;
|
||||
readonly scenarioId: XtreamScenarioId;
|
||||
readonly terminal: XtreamScenarioTerminalEvidence;
|
||||
readonly trigger: XtreamScenarioTriggerEvidence;
|
||||
}): XtreamScenarioAssembly {
|
||||
assertEnvelope(input);
|
||||
return {
|
||||
backgroundMetrics: backgroundMetrics(input),
|
||||
...cancellationEvidence(input),
|
||||
};
|
||||
}
|
||||
|
||||
function assertEnvelope(input: {
|
||||
readonly main: XtreamMainCaptureResult;
|
||||
readonly phaseCapture: XtreamPhaseAttributionInput;
|
||||
readonly renderer: XtreamRendererCaptureMetrics;
|
||||
readonly scenarioId: XtreamScenarioId;
|
||||
readonly terminal: XtreamScenarioTerminalEvidence;
|
||||
readonly trigger: XtreamScenarioTriggerEvidence;
|
||||
}): void {
|
||||
const start = input.phaseCapture.operationStartEpochMs;
|
||||
const terminal = input.phaseCapture.terminalEpochMs;
|
||||
if (
|
||||
input.trigger.scenarioId !== input.scenarioId ||
|
||||
input.terminal.scenarioId !== input.scenarioId ||
|
||||
input.renderer.probe.scenarioId !== input.scenarioId ||
|
||||
input.trigger.triggerEpochMs < Math.floor(start) ||
|
||||
input.trigger.triggerEpochMs > input.terminal.terminalEpochMs ||
|
||||
input.main.captureStartedEpochMs >
|
||||
input.main.measurementStartedEpochMs ||
|
||||
input.main.measurementStartedEpochMs > start ||
|
||||
input.renderer.measurementStartEpochMs < start ||
|
||||
input.renderer.probe.terminalEpochMs === null ||
|
||||
input.renderer.probe.terminalEpochMs >
|
||||
input.renderer.captureCutoffEpochMs ||
|
||||
input.renderer.captureCutoffEpochMs > input.main.captureCutoffEpochMs ||
|
||||
terminal > input.main.captureCutoffEpochMs ||
|
||||
input.main.captureCutoffEpochMs > input.main.captureStoppedEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
}
|
||||
|
||||
function backgroundMetrics(
|
||||
input: Parameters<typeof assembleXtreamScenarioEvidence>[0]
|
||||
): XtreamScenarioAssembly['backgroundMetrics'] {
|
||||
const background = input.terminal.backgroundProbe;
|
||||
const operation = input.terminal.operation;
|
||||
if (input.scenarioId !== XTREAM_SCENARIO_ID.BACKGROUND_UI) {
|
||||
if (background !== null || operation !== null) invalid();
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!background ||
|
||||
!operation ||
|
||||
operation.active ||
|
||||
operation.terminalStatus !== 'completed' ||
|
||||
operation.terminalEpochMs === null ||
|
||||
background.operation !== 'delete-xtream-content' ||
|
||||
operation.operation !== background.operation ||
|
||||
operation.operationId !== background.operationId ||
|
||||
operation.playlistId !== background.playlistId ||
|
||||
operation.startedEpochMs > background.startedEpochMs ||
|
||||
operation.terminalEpochMs > input.terminal.terminalEpochMs ||
|
||||
background.completedEpochMs > input.terminal.terminalEpochMs ||
|
||||
background.completedEpochMs >= input.renderer.probe.uiPaintedEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const requests = input.main.requests.filter(
|
||||
({ operation: candidate }) => candidate === 'DB_DELETE_XTREAM_CONTENT'
|
||||
);
|
||||
const request = requests[0];
|
||||
if (
|
||||
requests.length !== 1 ||
|
||||
!request ||
|
||||
request.operationId !== background.operationId ||
|
||||
request.playlistId !== background.playlistId ||
|
||||
request.workStartedEpochMs === null ||
|
||||
request.workEndedEpochMs === null ||
|
||||
background.startedEpochMs < request.workStartedEpochMs ||
|
||||
background.startedEpochMs > request.workEndedEpochMs ||
|
||||
request.workEndedEpochMs > input.terminal.terminalEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const result = readUiResult(background.result);
|
||||
const firstSample = result.samples[0];
|
||||
const lastSample = result.samples.at(-1);
|
||||
const dashboardSample = result.samples.find(
|
||||
({ kind }) => kind === 'dashboard'
|
||||
);
|
||||
const storeTerminalEpochMs = input.renderer.probe.storeTerminalEpochMs;
|
||||
if (
|
||||
!firstSample ||
|
||||
!lastSample ||
|
||||
!dashboardSample ||
|
||||
storeTerminalEpochMs === null ||
|
||||
firstSample.startEpochMs < background.startedEpochMs ||
|
||||
lastSample.endEpochMs > background.completedEpochMs ||
|
||||
lastSample.endEpochMs > storeTerminalEpochMs ||
|
||||
operation.terminalEpochMs > storeTerminalEpochMs ||
|
||||
request.workEndedEpochMs > storeTerminalEpochMs
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return {
|
||||
actionLatencyMs: requireNumber(result.action.max),
|
||||
dashboardPaintVisibility: result.dashboardPaintVisibility,
|
||||
navigationLatencyMs: requireNumber(result.navigation.max),
|
||||
searchLatencyMs: requireNumber(result.search.max),
|
||||
};
|
||||
}
|
||||
|
||||
function readUiResult(value: unknown): XtreamUiActionProbeResult {
|
||||
if (!isRecord(value)) invalid();
|
||||
const samplesValue = value['samples'];
|
||||
if (
|
||||
value['dashboardPaintVisibility'] !== 'visible-without-overlay' ||
|
||||
!Array.isArray(samplesValue) ||
|
||||
samplesValue.length !== XTREAM_UI_ACTION_ORDER.length
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const samples = samplesValue.map((entry, index) =>
|
||||
readSample(entry, index)
|
||||
);
|
||||
if (
|
||||
samples.some((sample, index) => {
|
||||
const previous = samples[index - 1];
|
||||
return (
|
||||
previous !== undefined &&
|
||||
previous.endEpochMs > sample.startEpochMs
|
||||
);
|
||||
})
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const summary = summarizeXtreamUiActionSamples(samples);
|
||||
if (
|
||||
!isDeepStrictEqual(summary.action, value['action']) ||
|
||||
!isDeepStrictEqual(summary.navigation, value['navigation']) ||
|
||||
!isDeepStrictEqual(summary.search, value['search'])
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return {
|
||||
...summary,
|
||||
dashboardPaintVisibility: 'visible-without-overlay',
|
||||
samples,
|
||||
};
|
||||
}
|
||||
|
||||
function readSample(value: unknown, index: number): XtreamUiActionSample {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
value['activeAfter'] !== true ||
|
||||
value['activeBefore'] !== true ||
|
||||
value['kind'] !== XTREAM_UI_ACTION_ORDER[index] ||
|
||||
!['action', 'navigation', 'search'].includes(String(value['category']))
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const startEpochMs = requireNumber(value['startEpochMs']);
|
||||
const endEpochMs = requireNumber(value['endEpochMs']);
|
||||
const latencyMs = requireNumber(value['latencyMs']);
|
||||
if (
|
||||
endEpochMs < startEpochMs ||
|
||||
Math.abs(latencyMs - (endEpochMs - startEpochMs)) > 0.001
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return {
|
||||
activeAfter: true,
|
||||
activeBefore: true,
|
||||
category: value['category'] as XtreamUiActionSample['category'],
|
||||
endEpochMs,
|
||||
kind: value['kind'] as XtreamUiActionSample['kind'],
|
||||
latencyMs,
|
||||
startEpochMs,
|
||||
};
|
||||
}
|
||||
|
||||
function cancellationEvidence(
|
||||
input: Parameters<typeof assembleXtreamScenarioEvidence>[0]
|
||||
): Pick<XtreamScenarioAssembly, 'cancellation' | 'cancellationEvidence'> {
|
||||
const driver = input.terminal.cancellation;
|
||||
if (input.scenarioId !== XTREAM_SCENARIO_ID.CANCEL_IMPORT) {
|
||||
if (driver !== null) invalid();
|
||||
return {
|
||||
cancellation: emptyCancellation(),
|
||||
cancellationEvidence: null,
|
||||
};
|
||||
}
|
||||
const failed = input.main.requests.filter(
|
||||
({ operation, success }) => operation === 'DB_SAVE_CONTENT' && !success
|
||||
);
|
||||
const request = failed[0];
|
||||
const timeline = input.main.cancelTimeline;
|
||||
const renderer = input.renderer.probe;
|
||||
if (
|
||||
failed.length !== 1 ||
|
||||
!request ||
|
||||
!driver ||
|
||||
typeof request.requestId !== 'string' ||
|
||||
typeof request.operationId !== 'string' ||
|
||||
driver.operationId !== request.operationId ||
|
||||
driver.armedEpochMs > driver.progressEpochMs ||
|
||||
driver.progressEpochMs > driver.clickEpochMs ||
|
||||
driver.threshold !== Math.max(100, Math.floor(driver.total * 0.1)) ||
|
||||
driver.current < driver.threshold ||
|
||||
renderer.cancellationClickEpochMs === null ||
|
||||
renderer.cancellationClickEpochMs < driver.clickEpochMs ||
|
||||
renderer.dbCancellationTerminalEpochMs === null ||
|
||||
timeline.length !== 3
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const dispatch = timeline[0];
|
||||
const receipt = timeline[1];
|
||||
const workerTerminal = timeline[2];
|
||||
const preload = ipcPair(input.phaseCapture.ipcSpans, 'dbCancelOperation');
|
||||
const authoritative = request.responseEpochMs;
|
||||
const painted = requireNumber(renderer.uiPaintedEpochMs);
|
||||
if (
|
||||
!dispatch ||
|
||||
!receipt ||
|
||||
!workerTerminal ||
|
||||
dispatch.type !== 'db-cancel-dispatched' ||
|
||||
receipt.type !== 'db-cancel-received' ||
|
||||
workerTerminal.type !== 'db-cancel-terminal-received' ||
|
||||
renderer.dbCancellationTerminalEpochMs < workerTerminal.epochMs ||
|
||||
renderer.dbCancellationTerminalEpochMs > painted ||
|
||||
authoritative > painted
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
const click = driver.clickEpochMs;
|
||||
const cancellation: XtreamCancellationMetrics = {
|
||||
acknowledgementLatencyMs: authoritative - click,
|
||||
authoritativeDatabaseAcknowledgementObserved: true,
|
||||
authoritativeTerminalEpochMs: authoritative,
|
||||
clickEpochMs: click,
|
||||
clickToDatabaseDispatchMs: dispatch.epochMs - click,
|
||||
clickToPaintMs: painted - click,
|
||||
clickToPreloadReturnMs: preload[1].endEpochMs - click,
|
||||
databaseDispatchEpochMs: dispatch.epochMs,
|
||||
failedDatabaseRequestId: request.requestId,
|
||||
failedOperationId: request.operationId,
|
||||
mainNetworkAbortObserved: false,
|
||||
mainNetworkAbortReason:
|
||||
XTREAM_CANCEL_NETWORK_ABORT_REASON.AFTER_NETWORK_COMPLETE,
|
||||
paintedEpochMs: painted,
|
||||
preloadReturnEpochMs: preload[1].endEpochMs,
|
||||
preloadStartEpochMs: preload[0].startEpochMs,
|
||||
workerReceiptEpochMs: receipt.epochMs,
|
||||
workerReceiptIsAuthoritative: false,
|
||||
workerReceiptLatencyMs: receipt.epochMs - click,
|
||||
};
|
||||
if (
|
||||
Object.values(cancellation).some(
|
||||
(entry) => typeof entry === 'number' && entry < 0
|
||||
)
|
||||
) {
|
||||
invalid();
|
||||
}
|
||||
return {
|
||||
cancellation,
|
||||
cancellationEvidence: {
|
||||
timeline: timeline.map((entry) => ({ ...entry })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ipcPair(
|
||||
spans: readonly XtreamIpcAttributionSpan[],
|
||||
method: string
|
||||
): readonly [XtreamIpcAttributionSpan, XtreamIpcAttributionSpan] {
|
||||
const selected = spans.filter((span) => span.method === method);
|
||||
const request = selected.find(({ boundary }) => boundary === 'request');
|
||||
const response = selected.find(({ boundary }) => boundary === 'response');
|
||||
if (selected.length !== 2 || !request || !response) invalid();
|
||||
return [request, response];
|
||||
}
|
||||
|
||||
function emptyCancellation(): XtreamCancellationMetrics {
|
||||
return {
|
||||
acknowledgementLatencyMs: null,
|
||||
authoritativeDatabaseAcknowledgementObserved: false,
|
||||
authoritativeTerminalEpochMs: null,
|
||||
clickEpochMs: null,
|
||||
clickToDatabaseDispatchMs: null,
|
||||
clickToPaintMs: null,
|
||||
clickToPreloadReturnMs: null,
|
||||
databaseDispatchEpochMs: null,
|
||||
failedDatabaseRequestId: null,
|
||||
failedOperationId: null,
|
||||
mainNetworkAbortObserved: false,
|
||||
mainNetworkAbortReason: null,
|
||||
paintedEpochMs: null,
|
||||
preloadReturnEpochMs: null,
|
||||
preloadStartEpochMs: null,
|
||||
workerReceiptEpochMs: null,
|
||||
workerReceiptIsAuthoritative: false,
|
||||
workerReceiptLatencyMs: null,
|
||||
};
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function requireNumber(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
||||
invalid();
|
||||
return value;
|
||||
}
|
||||
|
||||
function invalid(): never {
|
||||
throw new Error('invalid Xtream scenario assembly evidence');
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
import {
|
||||
XTREAM_ITERATION_KIND,
|
||||
XTREAM_SCENARIO_ID,
|
||||
type XtreamIterationDefinition,
|
||||
type XtreamScenarioId,
|
||||
} from './xtream-benchmark-contract';
|
||||
import { iteration } from './xtream-benchmark-report.fixtures';
|
||||
import { assertXtreamIterationResult } from './xtream-iteration-validity';
|
||||
import type { XtreamRawIterationResult } from './xtream-summary-contract';
|
||||
|
||||
describe('Xtream persisted iteration evidence binding', () => {
|
||||
it('rejects an independently edited phase headline', () => {
|
||||
const raw = createIteration();
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
phases: {
|
||||
...raw.phases,
|
||||
sqliteWriteMs: 99,
|
||||
},
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects worker phase evidence that no longer backs the phase headline', () => {
|
||||
const raw = createIteration();
|
||||
const requests = raw.databaseWorker.requests.evidence.map((request) => {
|
||||
if (request.operation !== 'DB_SAVE_CONTENT') return request;
|
||||
const phaseEvents = request.phaseEvents.map((event, index) =>
|
||||
index === 1
|
||||
? {
|
||||
...event,
|
||||
durationMs: 1.5,
|
||||
epochMs: event.epochMs + 0.5,
|
||||
}
|
||||
: event
|
||||
);
|
||||
return { ...request, phaseEvents };
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
databaseWorker: {
|
||||
...raw.databaseWorker,
|
||||
requests: {
|
||||
...raw.databaseWorker.requests,
|
||||
evidence: requests,
|
||||
},
|
||||
},
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a self-consistent cancel headline outside the failed request', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const cancellation = {
|
||||
...raw.cancellation,
|
||||
authoritativeTerminalEpochMs:
|
||||
Number(raw.cancellation.authoritativeTerminalEpochMs) + 5_000,
|
||||
clickEpochMs: Number(raw.cancellation.clickEpochMs) + 5_000,
|
||||
databaseDispatchEpochMs:
|
||||
Number(raw.cancellation.databaseDispatchEpochMs) + 5_000,
|
||||
paintedEpochMs: Number(raw.cancellation.paintedEpochMs) + 5_000,
|
||||
preloadReturnEpochMs:
|
||||
Number(raw.cancellation.preloadReturnEpochMs) + 5_000,
|
||||
preloadStartEpochMs:
|
||||
Number(raw.cancellation.preloadStartEpochMs) + 5_000,
|
||||
workerReceiptEpochMs:
|
||||
Number(raw.cancellation.workerReceiptEpochMs) + 5_000,
|
||||
};
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellation,
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
it('binds the dedicated cancel receipt and failed-request identities exactly', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const receiptDrift = {
|
||||
...raw.cancellation,
|
||||
workerReceiptEpochMs:
|
||||
Number(raw.cancellation.workerReceiptEpochMs) + 0.5,
|
||||
workerReceiptLatencyMs:
|
||||
Number(raw.cancellation.workerReceiptLatencyMs) + 0.5,
|
||||
};
|
||||
const wrongRequest = {
|
||||
...raw.cancellation,
|
||||
failedDatabaseRequestId: 'another-request',
|
||||
};
|
||||
const wrongOperation = {
|
||||
...raw.cancellation,
|
||||
failedOperationId: 'another-operation',
|
||||
};
|
||||
|
||||
for (const cancellation of [
|
||||
receiptDrift,
|
||||
wrongRequest,
|
||||
wrongOperation,
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellation,
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts either ordering of worker receipt and preload return after dispatch', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const dispatch = Number(raw.cancellation.databaseDispatchEpochMs);
|
||||
const preloadReturn = Number(raw.cancellation.preloadReturnEpochMs);
|
||||
const workerTerminal = Number(
|
||||
raw.cancellationEvidence?.timeline.find(
|
||||
({ type }) => type === 'db-cancel-terminal-received'
|
||||
)?.epochMs
|
||||
);
|
||||
const beforeReturn = dispatch + (preloadReturn - dispatch) / 2;
|
||||
const afterReturn =
|
||||
preloadReturn + (workerTerminal - preloadReturn) / 2;
|
||||
|
||||
assert.notEqual(
|
||||
raw.cancellation.workerReceiptEpochMs,
|
||||
preloadReturn,
|
||||
'fixture receipt must be independent from the preload response'
|
||||
);
|
||||
for (const receiptEpochMs of [beforeReturn, afterReturn]) {
|
||||
assert.doesNotThrow(() =>
|
||||
assertXtreamIterationResult(withReceipt(raw, receiptEpochMs))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects cancel-receipt identity drift and receipt after the authoritative terminal', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
const receiptIndex = evidence.timeline.findIndex(
|
||||
({ type }) => type === 'db-cancel-received'
|
||||
);
|
||||
assert.notEqual(receiptIndex, -1);
|
||||
|
||||
for (const receipt of [
|
||||
{ requestId: 'another-request' },
|
||||
{ operationId: 'another-operation' },
|
||||
{ ipcCallId: 999 },
|
||||
{ sourceEpochMs: 999 },
|
||||
{
|
||||
epochMs:
|
||||
Number(raw.cancellation.authoritativeTerminalEpochMs) + 1,
|
||||
},
|
||||
]) {
|
||||
const timeline = evidence.timeline.map((record, index) =>
|
||||
index === receiptIndex ? { ...record, ...receipt } : record
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellationEvidence: { timeline },
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
}
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellationEvidence: {
|
||||
timeline: evidence.timeline.map((record) => ({
|
||||
...record,
|
||||
sourceEpochMs: record.sourceEpochMs + 1,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects reuse of a worker request IPC identity for cancellation', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
const failedRequest = raw.databaseWorker.requests.evidence.find(
|
||||
({ operation, success }) =>
|
||||
operation === 'DB_SAVE_CONTENT' && !success
|
||||
);
|
||||
assert.ok(failedRequest?.ipcCallId);
|
||||
|
||||
for (const ipcCallId of [failedRequest.ipcCallId, 999]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellationEvidence: {
|
||||
timeline: evidence.timeline.map((record) => ({
|
||||
...record,
|
||||
ipcCallId: ipcCallId as number,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('persists producer identity on the database cancel IPC pair', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const databaseCancel = raw.phaseCapture.ipcSpans.filter(
|
||||
({ method }) => method === 'dbCancelOperation'
|
||||
) as readonly ((typeof raw.phaseCapture.ipcSpans)[number] & {
|
||||
readonly ipcCallId?: number;
|
||||
readonly sourceEpochMs?: number;
|
||||
})[];
|
||||
|
||||
assert.equal(databaseCancel.length, 2);
|
||||
assert.equal(typeof databaseCancel[0]?.ipcCallId, 'number');
|
||||
assert.equal(
|
||||
databaseCancel[0]?.ipcCallId,
|
||||
databaseCancel[1]?.ipcCallId
|
||||
);
|
||||
assert.equal(typeof databaseCancel[0]?.sourceEpochMs, 'number');
|
||||
assert.equal(
|
||||
databaseCancel[0]?.sourceEpochMs,
|
||||
databaseCancel[1]?.sourceEpochMs
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects database preload identities swapped across worker operations', () => {
|
||||
const raw = createIteration();
|
||||
const contentRead = raw.phaseCapture.ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbGetContent'
|
||||
);
|
||||
const contentWrite = raw.phaseCapture.ipcSpans.find(
|
||||
({ boundary, method }) =>
|
||||
boundary === 'request' && method === 'dbSaveContent'
|
||||
);
|
||||
assert.ok(contentRead);
|
||||
assert.ok(contentWrite);
|
||||
const ipcSpans = raw.phaseCapture.ipcSpans.map((span) => {
|
||||
if (span.correlationId === contentRead.correlationId) {
|
||||
return {
|
||||
...span,
|
||||
ipcCallId: contentWrite.ipcCallId,
|
||||
sourceEpochMs: contentWrite.sourceEpochMs,
|
||||
};
|
||||
}
|
||||
return span.correlationId === contentWrite.correlationId
|
||||
? {
|
||||
...span,
|
||||
ipcCallId: contentRead.ipcCallId,
|
||||
sourceEpochMs: contentRead.sourceEpochMs,
|
||||
}
|
||||
: span;
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
phaseCapture: { ...raw.phaseCapture, ipcSpans },
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects cancellation source chronology before an earlier worker call', () => {
|
||||
const raw = createIteration(XTREAM_SCENARIO_ID.CANCEL_IMPORT);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
const lastWorkerCall = raw.databaseWorker.requests.evidence
|
||||
.filter(
|
||||
(request) =>
|
||||
request.ipcCallId !== null && request.sourceEpochMs !== null
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
Number(right.ipcCallId) - Number(left.ipcCallId)
|
||||
)[0];
|
||||
assert.ok(lastWorkerCall?.sourceEpochMs);
|
||||
const databaseSourceEpochMs =
|
||||
Number(lastWorkerCall.sourceEpochMs) - 0.25;
|
||||
const networkSourceEpochMs = databaseSourceEpochMs - 0.25;
|
||||
const ipcSpans = raw.phaseCapture.ipcSpans.map((span) => {
|
||||
if (span.method === 'xtreamCancelSession') {
|
||||
return { ...span, sourceEpochMs: networkSourceEpochMs };
|
||||
}
|
||||
return span.method === 'dbCancelOperation'
|
||||
? { ...span, sourceEpochMs: databaseSourceEpochMs }
|
||||
: span;
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
cancellationEvidence: {
|
||||
timeline: evidence.timeline.map((record) => ({
|
||||
...record,
|
||||
sourceEpochMs: databaseSourceEpochMs,
|
||||
})),
|
||||
},
|
||||
phaseCapture: { ...raw.phaseCapture, ipcSpans },
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores caller-owned phase array methods when checking evidence', () => {
|
||||
const raw = createIteration();
|
||||
const events = [
|
||||
...raw.phaseCapture.events,
|
||||
] as unknown as typeof raw.phaseCapture.events & {
|
||||
filter: () => readonly never[];
|
||||
map: () => readonly never[];
|
||||
};
|
||||
Object.defineProperties(events, {
|
||||
filter: { value: () => [] },
|
||||
map: { value: () => [] },
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
assertXtreamIterationResult({
|
||||
...raw,
|
||||
phaseCapture: { ...raw.phaseCapture, events },
|
||||
phases: { ...raw.phases, sqliteWriteMs: 99 },
|
||||
}),
|
||||
/invalid Xtream iteration/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function withReceipt(
|
||||
raw: XtreamRawIterationResult,
|
||||
receiptEpochMs: number
|
||||
): XtreamRawIterationResult {
|
||||
const click = Number(raw.cancellation.clickEpochMs);
|
||||
const evidence = cancellationEvidence(raw);
|
||||
return {
|
||||
...raw,
|
||||
cancellation: {
|
||||
...raw.cancellation,
|
||||
workerReceiptEpochMs: receiptEpochMs,
|
||||
workerReceiptLatencyMs: receiptEpochMs - click,
|
||||
},
|
||||
cancellationEvidence: {
|
||||
timeline: evidence.timeline.map((record) =>
|
||||
record.type === 'db-cancel-received'
|
||||
? { ...record, epochMs: receiptEpochMs }
|
||||
: record
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function cancellationEvidence(
|
||||
raw: XtreamRawIterationResult
|
||||
): NonNullable<XtreamRawIterationResult['cancellationEvidence']> {
|
||||
const evidence = raw.cancellationEvidence;
|
||||
assert.ok(evidence);
|
||||
return evidence;
|
||||
}
|
||||
|
||||
function createIteration(
|
||||
scenarioId: XtreamScenarioId = XTREAM_SCENARIO_ID.INITIAL_IMPORT_LARGE
|
||||
): XtreamRawIterationResult {
|
||||
const definition: XtreamIterationDefinition = {
|
||||
eligibleForComparison: true,
|
||||
kind: XTREAM_ITERATION_KIND.MEASURED,
|
||||
runId: 'run-01',
|
||||
scenarioId,
|
||||
};
|
||||
return iteration(definition, 1);
|
||||
}
|
||||
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user