test(performance): add formal M3U import benchmark (#1287)

* test(performance): add formal M3U import benchmark

* test(performance): harden formal capture validity

* test(performance): address benchmark review feedback
This commit is contained in:
4gray authored and GitHub committed 2026-07-27 10:34:22 +02:00
1 parent 26331676f9
commit 24f0dee6f0
80 files changed
+11131 -192

No files matched your search

+2 -2
View File
@@ -85,8 +85,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 phase markers for refresh/DB benchmark correlation; benchmark tooling sets it explicitly, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker timestamps, thread CPU, event-loop utilization, and event-loop delay metrics in database and playlist-refresh responses, plus 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 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
- Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
+2 -2
View File
@@ -141,8 +141,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 phase markers for refresh/DB benchmark correlation; benchmark tooling sets it explicitly, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker timestamps, thread CPU, event-loop utilization, and event-loop delay metrics in database and playlist-refresh responses, plus 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 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
Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
+10
View File
@@ -29,6 +29,16 @@
"command": "pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-refresh-cancellation.performance.ts"
}
},
"benchmark-m3u-import": {
"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.performance.config.ts src/m3u-import.performance.ts"
}
},
"packaged-frame-copy-smoke": {
"dependsOn": ["test-packaged-frame-copy-fixtures"],
"executor": "nx:run-commands",
@@ -0,0 +1,8 @@
import { test } from '@playwright/test';
import { runM3uImportBenchmark } from './performance/m3u-import.benchmark';
// eslint-disable-next-line playwright/expect-expect -- Assertions run inside the shared benchmark lifecycle.
test('profiles initial M3U imports at 10k, 50k, and 100k', async () => {
await runM3uImportBenchmark();
});
@@ -52,11 +52,125 @@ test('correlates the real preload marker to a DB request whose payload omits ope
});
assert.deepEqual(identity, {
ipcCallId: 2,
operationId: 'refresh-operation-1',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
});
test('retains the initial-import upsert marker when its operationId is intentionally uncorrelated', () => {
const { api, ipcMain } = createCapture();
ipcMain.emit(
CHANNEL,
{},
marker({
correlationState: 'uncorrelated',
ipcCallId: 7,
method: 'dbUpsertAppPlaylist',
operationId: null,
sourceEpochMs: 1_077.25,
})
);
const identity = api.matchDatabaseRequest({
operation: 'DB_UPSERT_APP_PLAYLIST',
payload: { _id: 'playlist-1' },
requestId: 'initial-import-upsert',
type: 'request',
});
assert.deepEqual(identity, {
ipcCallId: 7,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-uncorrelated',
sourceEpochMs: 1_077.25,
});
});
test('captures exact successful preload completion markers for DB return-clone attribution', () => {
const { api, ipcMain } = createCapture();
ipcMain.emit(
CHANNEL,
{},
marker({
correlationState: 'uncorrelated',
ipcCallId: 7,
method: 'dbUpsertAppPlaylist',
operationId: null,
phase: 'success',
sourceEpochMs: 140.25,
})
);
ipcMain.emit(
CHANNEL,
{},
marker({
correlationState: 'uncorrelated',
ipcCallId: 8,
operationId: null,
phase: 'success',
sourceEpochMs: 175.5,
})
);
assert.equal(api.successMarkerCount(), 2);
assert.deepEqual(api.takeSuccessMarkers(), [
{
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
operationId: null,
playlistId: 'playlist-1',
sourceEpochMs: 140.25,
},
{
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
operationId: null,
playlistId: 'playlist-1',
sourceEpochMs: 175.5,
},
]);
assert.equal(api.successMarkerCount(), 0);
assert.deepEqual(api.takeSuccessMarkers(), []);
});
test('fails closed for malformed or invalid successful preload markers and resets completions', () => {
const { api, ipcMain } = createCapture();
for (const invalid of [
{ invalidReason: 'out-of-order' },
{ correlationState: 'invalid' },
{ ipcCallId: 0 },
{ playlistId: '' },
{ sourceEpochMs: Number.NaN },
{ method: 'refreshPlaylist' },
{ correlationState: 'correlated', operationId: null },
]) {
ipcMain.emit(
CHANNEL,
{},
marker({
phase: 'success',
...invalid,
})
);
}
assert.equal(api.successMarkerCount(), 0);
assert.deepEqual(api.takeSuccessMarkers(), []);
ipcMain.emit(
CHANNEL,
{},
marker({
phase: 'success',
})
);
api.stop();
api.start();
assert.equal(api.successMarkerCount(), 0);
assert.deepEqual(api.takeSuccessMarkers(), []);
});
test('resolves a response-retained request identity when the marker arrives later and resets between captures', () => {
const { api, ipcMain } = createCapture();
const identity = api.matchDatabaseRequest({
@@ -73,8 +187,10 @@ test('resolves a response-retained request identity when the marker arrives late
ipcMain.emit(CHANNEL, {}, marker());
assert.deepEqual(responseOutcome.identity, {
ipcCallId: 2,
operationId: 'refresh-operation-1',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
api.stop();
@@ -87,8 +203,10 @@ test('resolves a response-retained request identity when the marker arrives late
type: 'request',
});
assert.deepEqual(afterReset, {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-missing',
sourceEpochMs: null,
});
});
@@ -101,8 +219,10 @@ test('fails closed for missing and ambiguous preload markers', () => {
type: 'request',
});
assert.deepEqual(missing, {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-missing',
sourceEpochMs: null,
});
const ambiguousCapture = createCapture();
@@ -122,8 +242,10 @@ test('fails closed for missing and ambiguous preload markers', () => {
type: 'request',
});
assert.deepEqual(ambiguous, {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-ambiguous',
sourceEpochMs: null,
});
});
@@ -191,19 +313,25 @@ test('quarantines a key after response-before-marker ambiguity', () => {
});
const ambiguousIdentity = {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-ambiguous',
sourceEpochMs: null,
};
assert.deepEqual(requestA, ambiguousIdentity);
assert.deepEqual(requestB, ambiguousIdentity);
assert.deepEqual(requestC, ambiguousIdentity);
assert.deepEqual(otherPlaylist, {
ipcCallId: 4,
operationId: 'other-playlist-operation',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
assert.deepEqual(otherOperation, {
ipcCallId: 5,
operationId: 'other-db-operation',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
api.stop();
@@ -223,7 +351,9 @@ test('quarantines a key after response-before-marker ambiguity', () => {
type: 'request',
});
assert.deepEqual(freshCaptureIdentity, {
ipcCallId: 6,
operationId: 'fresh-capture-operation',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
});
@@ -4,14 +4,26 @@ export const DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY =
'__iptvnatorM3uRefreshDatabaseRequestIdentityCapture';
export interface DatabaseRequestIdentity {
ipcCallId: number | null;
operationId: string | null;
operationIdUnavailableReason: string | null;
sourceEpochMs: number | null;
}
export interface PreloadDatabaseSuccessMarker {
ipcCallId: number;
operation: 'DB_GET_APP_PLAYLIST' | 'DB_UPSERT_APP_PLAYLIST';
operationId: string | null;
playlistId: string;
sourceEpochMs: number;
}
export interface DatabaseRequestIdentityCaptureApi {
matchDatabaseRequest(message: unknown): DatabaseRequestIdentity;
start(): void;
stop(): void;
successMarkerCount(): number;
takeSuccessMarkers(): readonly PreloadDatabaseSuccessMarker[];
}
interface DatabaseRequestIdentityElectron {
@@ -31,8 +43,10 @@ export function installDatabaseRequestIdentityCaptureInMain(
interface PendingMarker {
readonly ipcCallId: number;
readonly operation: string;
readonly operationId: string;
readonly operationId: string | null;
readonly operationIdUnavailableReason: string | null;
readonly playlistId: string;
readonly sourceEpochMs: number;
}
interface PendingRequest {
readonly identity: DatabaseRequestIdentity;
@@ -58,6 +72,7 @@ export function installDatabaseRequestIdentityCaptureInMain(
let pendingMarkers: PendingMarker[] = [];
let pendingRequests: PendingRequest[] = [];
let quarantinedKeys: QuarantinedKey[] = [];
let successMarkers: PreloadDatabaseSuccessMarker[] = [];
const readRecord = (value: unknown): JsonRecord | null =>
typeof value === 'object' && value !== null
@@ -88,11 +103,37 @@ export function installDatabaseRequestIdentityCaptureInMain(
candidate.playlistId === playlistId;
const markAmbiguous = (requests: PendingRequest[]): void => {
for (const request of requests) {
request.identity.ipcCallId = null;
request.identity.operationId = null;
request.identity.operationIdUnavailableReason =
'preload-performance-marker-ambiguous';
request.identity.sourceEpochMs = null;
}
};
const unavailableIdentity = (reason: string): DatabaseRequestIdentity => ({
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: reason,
sourceEpochMs: null,
});
const identityFromMarker = (
marker: PendingMarker
): DatabaseRequestIdentity => ({
ipcCallId: marker.ipcCallId,
operationId: marker.operationId,
operationIdUnavailableReason: marker.operationIdUnavailableReason,
sourceEpochMs: marker.sourceEpochMs,
});
const applyMarker = (
identity: DatabaseRequestIdentity,
marker: PendingMarker
): void => {
identity.ipcCallId = marker.ipcCallId;
identity.operationId = marker.operationId;
identity.operationIdUnavailableReason =
marker.operationIdUnavailableReason;
identity.sourceEpochMs = marker.sourceEpochMs;
};
const isQuarantined = (operation: string, playlistId: string): boolean =>
quarantinedKeys.some((key) => matches(key, operation, playlistId));
const quarantine = (operation: string, playlistId: string): void => {
@@ -116,32 +157,86 @@ export function installDatabaseRequestIdentityCaptureInMain(
return;
}
const marker = readRecord(input);
const correlationState = marker?.['correlationState'];
const operation =
typeof marker?.['method'] === 'string'
? markerMethodToOperation[marker['method']]
: undefined;
const operationId = marker?.['operationId'];
const playlistId = marker?.['playlistId'];
const ipcCallId = marker?.['ipcCallId'];
const sourceEpochMs = marker?.['sourceEpochMs'];
const validMarkerIdentity =
operation !== undefined &&
typeof playlistId === 'string' &&
playlistId.length > 0 &&
Number.isSafeInteger(ipcCallId) &&
Number(ipcCallId) >= 1 &&
typeof sourceEpochMs === 'number' &&
Number.isFinite(sourceEpochMs) &&
sourceEpochMs >= 0;
if (
marker?.['phase'] === 'success' &&
(correlationState === 'correlated' ||
correlationState === 'uncorrelated') &&
marker['invalidReason'] === null &&
validMarkerIdentity &&
(correlationState !== 'correlated' ||
(typeof operationId === 'string' &&
operationId.length > 0))
) {
successMarkers.push({
ipcCallId: Number(ipcCallId),
operation: operation as PreloadDatabaseSuccessMarker['operation'],
operationId:
correlationState === 'correlated'
? String(operationId)
: null,
playlistId,
sourceEpochMs,
});
return;
}
if (
!marker ||
marker['phase'] !== 'start' ||
marker['correlationState'] !== 'correlated' ||
(correlationState !== 'correlated' &&
correlationState !== 'uncorrelated') ||
marker['invalidReason'] !== null
) {
return;
}
const operation =
typeof marker['method'] === 'string'
? markerMethodToOperation[marker['method']]
: undefined;
const operationId = marker['operationId'];
const playlistId = marker['playlistId'];
const ipcCallId = marker['ipcCallId'];
const correlatedOperationId =
correlationState === 'correlated' &&
typeof operationId === 'string' &&
operationId.length > 0
? operationId
: null;
if (
operation === undefined ||
typeof operationId !== 'string' ||
operationId.length === 0 ||
(correlationState === 'correlated' &&
correlatedOperationId === null) ||
typeof playlistId !== 'string' ||
playlistId.length === 0 ||
!Number.isSafeInteger(ipcCallId) ||
Number(ipcCallId) < 1
Number(ipcCallId) < 1 ||
typeof sourceEpochMs !== 'number' ||
!Number.isFinite(sourceEpochMs) ||
sourceEpochMs < 0
) {
return;
}
const pendingMarker: PendingMarker = {
ipcCallId: Number(ipcCallId),
operation,
operationId: correlatedOperationId,
operationIdUnavailableReason:
correlationState === 'correlated'
? null
: 'preload-performance-marker-uncorrelated',
playlistId,
sourceEpochMs,
};
if (isQuarantined(operation, playlistId)) {
return;
@@ -154,8 +249,7 @@ export function installDatabaseRequestIdentityCaptureInMain(
if (!request) {
return;
}
request.identity.operationId = operationId;
request.identity.operationIdUnavailableReason = null;
applyMarker(request.identity, pendingMarker);
pendingRequests = pendingRequests.filter(
(candidate) => candidate !== request
);
@@ -166,12 +260,7 @@ export function installDatabaseRequestIdentityCaptureInMain(
return;
}
pendingMarkers.push({
ipcCallId: Number(ipcCallId),
operation,
operationId,
playlistId,
});
pendingMarkers.push(pendingMarker);
pendingMarkers.sort((left, right) => left.ipcCallId - right.ipcCallId);
});
@@ -189,19 +278,15 @@ export function installDatabaseRequestIdentityCaptureInMain(
operation !== 'DB_UPSERT_APP_PLAYLIST') ||
playlistId === null
) {
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-not-applicable',
};
return unavailableIdentity(
'preload-performance-marker-not-applicable'
);
}
if (isQuarantined(operation, playlistId)) {
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-ambiguous',
};
return unavailableIdentity(
'preload-performance-marker-ambiguous'
);
}
const markerCandidates = pendingMarkers.filter((marker) =>
matches(marker, operation, playlistId)
@@ -209,34 +294,25 @@ export function installDatabaseRequestIdentityCaptureInMain(
if (markerCandidates.length === 1) {
const marker = markerCandidates[0];
if (!marker) {
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-missing',
};
return unavailableIdentity(
'preload-performance-marker-missing'
);
}
pendingMarkers = pendingMarkers.filter(
(candidate) => candidate !== marker
);
return {
operationId: marker.operationId,
operationIdUnavailableReason: null,
};
return identityFromMarker(marker);
}
if (markerCandidates.length > 1) {
quarantine(operation, playlistId);
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-ambiguous',
};
return unavailableIdentity(
'preload-performance-marker-ambiguous'
);
}
const identity: DatabaseRequestIdentity = {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-missing',
};
const identity = unavailableIdentity(
'preload-performance-marker-missing'
);
pendingRequests.push({
identity,
operation,
@@ -248,6 +324,7 @@ export function installDatabaseRequestIdentityCaptureInMain(
pendingMarkers = [];
pendingRequests = [];
quarantinedKeys = [];
successMarkers = [];
active = true;
},
stop: () => {
@@ -255,6 +332,13 @@ export function installDatabaseRequestIdentityCaptureInMain(
pendingMarkers = [];
pendingRequests = [];
quarantinedKeys = [];
successMarkers = [];
},
successMarkerCount: () => successMarkers.length,
takeSuccessMarkers: () => {
const captured = Object.freeze([...successMarkers]);
successMarkers = [];
return captured;
},
};
target[stateKey] = api;
@@ -0,0 +1,98 @@
/* 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('counts only finite nonnegative worker heap and external-memory samples per capture generation', () => {
const workerRecord = section(
'interface WorkerRecord',
'interface TimelineRecord'
);
const createRecord = section(
'const record: WorkerRecord = {',
'nextWorkerOrdinal += 1'
);
const sampleWorker = section(
'const sampleWorker =',
'const resetWorkerForCapture'
);
const resetWorker = section(
'const resetWorkerForCapture',
'const startWorker'
);
assert.match(workerRecord, /externalMemorySampleCount: number/);
assert.match(workerRecord, /heapUsedSampleCount: number/);
assert.match(createRecord, /externalMemorySampleCount: 0/);
assert.match(createRecord, /heapUsedSampleCount: 0/);
assert.match(resetWorker, /record\.externalMemorySampleCount = 0/);
assert.match(resetWorker, /record\.heapUsedSampleCount = 0/);
assert.match(sampleWorker, /const heapUsedSample = stats\.used_heap_size/);
assert.match(
sampleWorker,
/Number\.isFinite\(heapUsedSample\)[\s\S]*heapUsedSample >= 0/
);
assert.match(sampleWorker, /record\.heapUsedSampleCount \+= 1/);
assert.match(
sampleWorker,
/const externalMemorySample = stats\.external_memory/
);
assert.match(
sampleWorker,
/Number\.isFinite\(externalMemorySample\)[\s\S]*externalMemorySample >= 0/
);
assert.match(sampleWorker, /record\.externalMemorySampleCount \+= 1/);
assert.doesNotMatch(sampleWorker, /used_heap_size \?\? 0/);
assert.doesNotMatch(sampleWorker, /external_memory \?\? 0/);
});
test('raw worker metrics distinguish missing samples from measured zero-byte peaks', () => {
const transport = section(
'const workers = currentWorkerRecords',
'const transport: MainCaptureGenerationTransport'
);
assert.match(
transport,
/externalMemorySampleCount:\s*record\.externalMemorySampleCount/
);
assert.match(
transport,
/heapUsedSampleCount:\s*record\.heapUsedSampleCount/
);
assert.match(
transport,
/peakExternalBytes:\s*record\.externalMemorySampleCount > 0[\s\S]*\?\s*record\.externalPeak[\s\S]*:\s*null/
);
assert.match(
transport,
/peakHeapUsedBytes:\s*record\.heapUsedSampleCount > 0[\s\S]*\?\s*record\.heapPeak[\s\S]*:\s*null/
);
assert.match(
transport,
/peakExternalUnavailableReason:\s*record\.externalMemorySampleCount > 0[\s\S]*\?\s*null[\s\S]*:\s*'worker-external-memory-samples-missing'/
);
assert.match(
transport,
/peakHeapUsedUnavailableReason:\s*record\.heapUsedSampleCount > 0[\s\S]*\?\s*null[\s\S]*:\s*'worker-heap-used-samples-missing'/
);
assert.doesNotMatch(
transport,
/peakExternalBytes: record\.externalPeak[,}]/
);
assert.doesNotMatch(transport, /peakHeapUsedBytes: record\.heapPeak[,}]/);
});
@@ -0,0 +1,121 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
assessDatabaseWorkerPeakMemoryValidity,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON,
} from './database-worker-peak-memory-validity';
describe('database worker peak-memory validity', () => {
it('requires successful heap and external-memory samples in every measured run', () => {
const validity = assessDatabaseWorkerPeakMemoryValidity([
iteration('warmup', 'warmup', worker(0, 0)),
iteration('run-01', 'measured', worker(3, 4)),
iteration('diagnostic', 'diagnostic', worker(0, 0)),
]);
assert.deepEqual(validity, {
invalidMeasuredRuns: [],
measuredRunCount: 1,
validForComparison: true,
validMeasuredRunCount: 1,
});
});
it('fails closed for absent samples instead of accepting initialized zero peaks', () => {
const missingCounts = assessDatabaseWorkerPeakMemoryValidity([
iteration('run-01', 'measured', {
kind: 'database.worker',
peakExternalBytes: 0,
peakHeapUsedBytes: 0,
}),
iteration('run-02', 'measured', worker(0, 1)),
iteration('run-03', 'measured', worker(1, 0)),
]);
assert.deepEqual(missingCounts.invalidMeasuredRuns, [
{
reason: DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_SAMPLES_MISSING,
runId: 'run-01',
},
{
reason: DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_SAMPLES_MISSING,
runId: 'run-02',
},
{
reason: DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_SAMPLES_MISSING,
runId: 'run-03',
},
]);
assert.equal(missingCounts.validForComparison, false);
assert.equal(missingCounts.validMeasuredRunCount, 0);
});
it('reports stable worker-cardinality and malformed-peak reasons', () => {
const validity = assessDatabaseWorkerPeakMemoryValidity([
{ kind: 'measured', main: { workers: [] }, runId: 'missing' },
{
kind: 'measured',
main: { workers: [worker(1, 1), worker(1, 1)] },
runId: 'multiple',
},
iteration('heap-invalid', 'measured', {
...worker(1, 1),
peakHeapUsedBytes: Number.NaN,
}),
iteration('external-invalid', 'measured', {
...worker(1, 1),
peakExternalBytes: -1,
}),
iteration('heap-reason-incoherent', 'measured', {
...worker(1, 1),
peakHeapUsedUnavailableReason:
'worker-heap-used-samples-missing',
}),
iteration('external-reason-incoherent', 'measured', {
...worker(1, 1),
peakExternalUnavailableReason:
'worker-external-memory-samples-missing',
}),
]);
assert.deepEqual(
validity.invalidMeasuredRuns.map(({ reason }) => reason),
[
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.DATABASE_WORKER_MISSING,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.MULTIPLE_DATABASE_WORKERS,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_PEAK_INVALID,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_PEAK_INVALID,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_PEAK_INVALID,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_PEAK_INVALID,
]
);
});
});
function iteration(
runId: string,
kind: string,
databaseWorker: Record<string, unknown>
) {
return {
kind,
main: { workers: [databaseWorker] },
runId,
};
}
function worker(
heapUsedSampleCount: number,
externalMemorySampleCount: number
) {
return {
externalMemorySampleCount,
heapUsedSampleCount,
kind: 'database.worker',
peakExternalBytes: 600,
peakExternalUnavailableReason: null,
peakHeapUsedBytes: 1_500,
peakHeapUsedUnavailableReason: null,
};
}
@@ -0,0 +1,114 @@
export const DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON = {
EXTERNAL_MEMORY_PEAK_INVALID:
'database-worker-external-memory-peak-invalid',
EXTERNAL_MEMORY_SAMPLES_MISSING:
'database-worker-external-memory-samples-missing',
HEAP_PEAK_INVALID: 'database-worker-heap-peak-invalid',
HEAP_SAMPLES_MISSING: 'database-worker-heap-samples-missing',
MULTIPLE_DATABASE_WORKERS: 'multiple-database-workers',
DATABASE_WORKER_MISSING: 'database-worker-missing',
} as const;
export type DatabaseWorkerPeakMemoryInvalidReason =
(typeof DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON)[keyof typeof DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON];
export interface DatabaseWorkerPeakMemoryValidity {
readonly invalidMeasuredRuns: readonly {
readonly reason: DatabaseWorkerPeakMemoryInvalidReason;
readonly runId: string;
}[];
readonly measuredRunCount: number;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
interface PeakMemoryWorker {
readonly externalMemorySampleCount?: unknown;
readonly heapUsedSampleCount?: unknown;
readonly kind: string;
readonly peakExternalBytes: unknown;
readonly peakExternalUnavailableReason?: unknown;
readonly peakHeapUsedBytes: unknown;
readonly peakHeapUsedUnavailableReason?: unknown;
}
interface PeakMemoryIteration {
readonly kind: string;
readonly main: {
readonly workers: readonly PeakMemoryWorker[];
};
readonly runId: string;
}
export function assessDatabaseWorkerPeakMemoryValidity(
iterations: readonly PeakMemoryIteration[]
): DatabaseWorkerPeakMemoryValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === 'measured'
);
const invalidMeasuredRuns: {
reason: DatabaseWorkerPeakMemoryInvalidReason;
runId: string;
}[] = [];
for (const iteration of measured) {
const workers = iteration.main.workers.filter(
(worker) => worker.kind === 'database.worker'
);
const reason = assessWorkers(workers);
if (reason !== null) {
invalidMeasuredRuns.push({ reason, runId: iteration.runId });
}
}
return Object.freeze({
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
validForComparison:
measured.length > 0 && invalidMeasuredRuns.length === 0,
validMeasuredRunCount: measured.length - invalidMeasuredRuns.length,
});
}
function assessWorkers(
workers: readonly PeakMemoryWorker[]
): DatabaseWorkerPeakMemoryInvalidReason | null {
if (workers.length === 0) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.DATABASE_WORKER_MISSING;
}
if (workers.length !== 1) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.MULTIPLE_DATABASE_WORKERS;
}
const worker = workers[0];
if (!worker) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.DATABASE_WORKER_MISSING;
}
if (!isPositiveSampleCount(worker.heapUsedSampleCount)) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_SAMPLES_MISSING;
}
if (
!isPeak(worker.peakHeapUsedBytes) ||
worker.peakHeapUsedUnavailableReason !== null
) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_PEAK_INVALID;
}
if (!isPositiveSampleCount(worker.externalMemorySampleCount)) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_SAMPLES_MISSING;
}
if (
!isPeak(worker.peakExternalBytes) ||
worker.peakExternalUnavailableReason !== null
) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_PEAK_INVALID;
}
return null;
}
function isPositiveSampleCount(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) > 0;
}
function isPeak(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
@@ -0,0 +1,127 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
assessDatabaseWorkerRequestMetricsValidity,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON,
} from './database-worker-request-metrics-validity';
import { createCompleteTestMainCapture } from './m3u-import-report.test-helpers';
import type {
WorkerCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
describe('database worker request-metrics validity', () => {
it('requires exactly two complete request samples in every measured run', () => {
const worker = completeWorker();
const validity = assessDatabaseWorkerRequestMetricsValidity([
iteration('warmup', 'warmup', [worker]),
iteration('run-01', 'measured', [worker]),
iteration('run-02', 'measured', [worker]),
iteration('diagnostic', 'diagnostic', [worker]),
]);
assert.deepEqual(validity, {
expectedRequestCount: 4,
invalidRequests: [],
measuredRunCount: 2,
validForComparison: true,
validMeasuredRunCount: 2,
validRequestCount: 4,
});
});
it('fails each measured run with missing ELD, ELU, CPU, or raw capture metrics', () => {
const validity = assessDatabaseWorkerRequestMetricsValidity([
invalidRequestIteration('run-eld', {
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
'event-loop-delay-capture-unavailable',
histogramFlushedEpochMs: null,
}),
invalidRequestIteration('run-elu', {
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
'event-loop-utilization-unavailable',
}),
invalidRequestIteration('run-cpu', {
threadCpuSystemMicros: null,
threadCpuUnavailableReason: 'thread-cpu-usage-unavailable',
threadCpuUserMicros: null,
}),
invalidRequestIteration('run-capture', {
performanceCaptureUnavailableReason:
'worker-performance-capture-invalid',
}),
]);
assert.equal(validity.expectedRequestCount, 8);
assert.equal(validity.validRequestCount, 4);
assert.equal(validity.validMeasuredRunCount, 0);
assert.equal(validity.validForComparison, false);
assert.deepEqual(
validity.invalidRequests.map(({ reason }) => reason),
[
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_DELAY_INVALID,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_UTILIZATION_INVALID,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.THREAD_CPU_INVALID,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.WORKER_CAPTURE_INVALID,
]
);
});
it('fails closed for missing, multiple, and wrong request sets', () => {
const worker = completeWorker();
const validity = assessDatabaseWorkerRequestMetricsValidity([
iteration('missing', 'measured', []),
iteration('multiple', 'measured', [worker, worker]),
iteration('wrong-requests', 'measured', [
{
...worker,
requests: worker.requests.slice(0, 1),
},
]),
]);
assert.equal(validity.expectedRequestCount, 6);
assert.equal(validity.validRequestCount, 0);
assert.equal(validity.validForComparison, false);
assert.deepEqual(
validity.invalidRequests.map(({ reason }) => reason),
[
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.DATABASE_WORKER_MISSING,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.MULTIPLE_DATABASE_WORKERS,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.REQUEST_SET_INVALID,
]
);
});
});
function completeWorker(): WorkerCaptureMetrics {
const worker = createCompleteTestMainCapture().workers[0];
assert.ok(worker);
return worker;
}
function invalidRequestIteration(
runId: string,
updates: Partial<WorkerRequestPerformanceMetrics>
) {
const worker = completeWorker();
const get = worker.requests[1];
assert.ok(get);
return iteration(runId, 'measured', [
{
...worker,
requests: [worker.requests[0], { ...get, ...updates }],
},
]);
}
function iteration(
runId: string,
kind: string,
workers: readonly WorkerCaptureMetrics[]
) {
return { kind, main: { workers }, runId };
}
@@ -0,0 +1,197 @@
import type {
WorkerCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
} from './m3u-refresh-cancellation-contract';
export const DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON = {
DATABASE_WORKER_MISSING: 'database-worker-missing',
EVENT_LOOP_DELAY_INVALID: 'event-loop-delay-invalid',
EVENT_LOOP_UTILIZATION_INVALID: 'event-loop-utilization-invalid',
MULTIPLE_DATABASE_WORKERS: 'multiple-database-workers',
REQUEST_SET_INVALID: 'initial-import-request-set-invalid',
THREAD_CPU_INVALID: 'thread-cpu-invalid',
WORKER_CAPTURE_INVALID: 'worker-performance-capture-invalid',
} as const;
export type DatabaseWorkerRequestMetricsInvalidReason =
(typeof DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON)[keyof typeof DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON];
export interface DatabaseWorkerRequestMetricsValidity {
readonly expectedRequestCount: number;
readonly invalidRequests: readonly {
readonly operation: string | null;
readonly reason: DatabaseWorkerRequestMetricsInvalidReason;
readonly runId: string;
}[];
readonly measuredRunCount: number;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
readonly validRequestCount: number;
}
interface RequestMetricsIteration {
readonly kind: string;
readonly main: {
readonly workers: readonly WorkerCaptureMetrics[];
};
readonly runId: string;
}
export function assessDatabaseWorkerRequestMetricsValidity(
iterations: readonly RequestMetricsIteration[]
): DatabaseWorkerRequestMetricsValidity {
const measured = iterations.filter(
(iteration) =>
iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
);
const invalidRequests: {
operation: string | null;
reason: DatabaseWorkerRequestMetricsInvalidReason;
runId: string;
}[] = [];
let validMeasuredRunCount = 0;
let validRequestCount = 0;
for (const iteration of measured) {
const workers = iteration.main.workers.filter(
(worker) => worker.kind === PERFORMANCE_WORKER_KIND.DATABASE
);
if (workers.length === 0) {
invalidRequests.push({
operation: null,
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.DATABASE_WORKER_MISSING,
runId: iteration.runId,
});
continue;
}
const worker = workers.length === 1 ? workers[0] : null;
if (!worker) {
invalidRequests.push({
operation: null,
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.MULTIPLE_DATABASE_WORKERS,
runId: iteration.runId,
});
continue;
}
const requests = requireInitialImportRequestPair(worker);
if (requests === null) {
invalidRequests.push({
operation: null,
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.REQUEST_SET_INVALID,
runId: iteration.runId,
});
continue;
}
let runIsValid = true;
for (const request of requests) {
const reason = assessRequest(request);
if (reason === null) {
validRequestCount += 1;
} else {
runIsValid = false;
invalidRequests.push({
operation: request.operation,
reason,
runId: iteration.runId,
});
}
}
if (runIsValid) {
validMeasuredRunCount += 1;
}
}
const expectedRequestCount = measured.length * 2;
return Object.freeze({
expectedRequestCount,
invalidRequests: Object.freeze(invalidRequests),
measuredRunCount: measured.length,
validForComparison:
measured.length > 0 &&
validMeasuredRunCount === measured.length &&
validRequestCount === expectedRequestCount,
validMeasuredRunCount,
validRequestCount,
});
}
function requireInitialImportRequestPair(
worker: WorkerCaptureMetrics
): readonly [
WorkerRequestPerformanceMetrics,
WorkerRequestPerformanceMetrics,
] | null {
const upserts = worker.requests.filter(
(request) =>
request.operation === 'DB_UPSERT_APP_PLAYLIST' && request.success
);
const gets = worker.requests.filter(
(request) =>
request.operation === 'DB_GET_APP_PLAYLIST' && request.success
);
return worker.requests.length === 2 &&
upserts.length === 1 &&
upserts[0] &&
gets.length === 1 &&
gets[0]
? [upserts[0], gets[0]]
: null;
}
function assessRequest(
request: WorkerRequestPerformanceMetrics
): DatabaseWorkerRequestMetricsInvalidReason | null {
if (
request.performanceCaptureUnavailableReason !== null ||
request.invalidReason !== null
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.WORKER_CAPTURE_INVALID;
}
if (
request.eventLoopDelayUnavailableReason !== null ||
request.histogramFlushedEpochMs === null ||
!isEventLoopDelay(request.eventLoopDelay)
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_DELAY_INVALID;
}
if (
request.eventLoopUtilizationUnavailableReason !== null ||
!isUtilization(request.eventLoopUtilization)
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_UTILIZATION_INVALID;
}
if (
request.threadCpuUnavailableReason !== null ||
!isFiniteNonNegative(request.threadCpuSystemMicros) ||
!isFiniteNonNegative(request.threadCpuUserMicros)
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.THREAD_CPU_INVALID;
}
return null;
}
function isEventLoopDelay(
value: WorkerRequestPerformanceMetrics['eventLoopDelay']
): value is NonNullable<WorkerRequestPerformanceMetrics['eventLoopDelay']> {
return (
value !== null &&
isFiniteNonNegative(value.maxMs) &&
isFiniteNonNegative(value.p95Ms) &&
isFiniteNonNegative(value.p99Ms) &&
value.p95Ms <= value.p99Ms &&
value.p99Ms <= value.maxMs
);
}
function isUtilization(value: number | null): value is number {
return isFiniteNonNegative(value) && value <= 1;
}
function isFiniteNonNegative(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
@@ -0,0 +1,185 @@
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function isCpuProfile(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
const startTime = value['startTime'];
const endTime = value['endTime'];
const nodes = value['nodes'];
const samples = value['samples'];
const timeDeltas = value['timeDeltas'];
if (
!isNonNegativeSafeInteger(startTime) ||
!isNonNegativeSafeInteger(endTime) ||
endTime <= startTime ||
!Array.isArray(nodes) ||
nodes.length === 0 ||
!Array.isArray(samples) ||
samples.length === 0 ||
!Array.isArray(timeDeltas) ||
timeDeltas.length !== samples.length
) {
return false;
}
const nodeIds = new Set<number>();
for (const node of nodes) {
if (!isCpuProfileNode(node) || nodeIds.has(node['id'])) {
return false;
}
nodeIds.add(node['id']);
}
if (
nodes.some((node) =>
(node as Record<string, unknown>)['children'] instanceof Array
? (
(node as Record<string, unknown>)[
'children'
] as unknown[]
).some(
(childId) =>
!isNonNegativeSafeInteger(childId) ||
!nodeIds.has(childId)
)
: false
) ||
samples.some(
(sampleId) =>
!isNonNegativeSafeInteger(sampleId) ||
!nodeIds.has(sampleId)
)
) {
return false;
}
const duration = endTime - startTime;
let elapsed = 0;
for (const delta of timeDeltas) {
if (!Number.isSafeInteger(delta)) {
return false;
}
elapsed += delta as number;
if (
!Number.isSafeInteger(elapsed) ||
elapsed < 0 ||
elapsed > duration
) {
return false;
}
}
return elapsed > 0;
}
export function isHeapSnapshot(value: unknown): boolean {
if (
!isRecord(value) ||
!isRecord(value['snapshot']) ||
!isRecord(value['snapshot']['meta'])
) {
return false;
}
const meta = value['snapshot']['meta'];
const nodeFields = meta['node_fields'];
const nodeTypes = meta['node_types'];
const edgeFields = meta['edge_fields'];
const edgeTypes = meta['edge_types'];
const nodes = value['nodes'];
const edges = value['edges'];
const strings = value['strings'];
return (
isNonEmptyStringArray(nodeFields) &&
isHeapTypeTable(nodeTypes, nodeFields.length) &&
isNonEmptyStringArray(edgeFields) &&
isHeapTypeTable(edgeTypes, edgeFields.length) &&
Array.isArray(nodes) &&
nodes.length > 0 &&
nodes.length % nodeFields.length === 0 &&
Array.isArray(edges) &&
edges.length > 0 &&
edges.length % edgeFields.length === 0 &&
Array.isArray(strings) &&
strings.length > 0
);
}
export function isRendererTrace(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
const events = value['traceEvents'];
return (
Array.isArray(events) &&
events.length > 0 &&
events.every(
(event) =>
isRecord(event) &&
isNonEmptyString(event['name']) &&
isNonEmptyString(event['ph']) &&
isNonNegativeFiniteNumber(event['ts'])
)
);
}
function isHeapTypeTable(value: unknown, fieldCount: number): boolean {
return (
Array.isArray(value) &&
value.length === fieldCount &&
value.every(
(entry) =>
isNonEmptyString(entry) || isNonEmptyStringArray(entry)
)
);
}
function isCpuProfileNode(
value: unknown
): value is Record<string, unknown> & { readonly id: number } {
if (
!isRecord(value) ||
!isNonNegativeSafeInteger(value['id']) ||
!isRecord(value['callFrame'])
) {
return false;
}
const callFrame = value['callFrame'];
const scriptId = callFrame['scriptId'];
const children = value['children'];
const hitCount = value['hitCount'];
return (
typeof callFrame['functionName'] === 'string' &&
(typeof scriptId === 'string' ||
isNonNegativeSafeInteger(scriptId)) &&
typeof callFrame['url'] === 'string' &&
Number.isSafeInteger(callFrame['lineNumber']) &&
Number.isSafeInteger(callFrame['columnNumber']) &&
(children === undefined ||
(Array.isArray(children) &&
children.every(isNonNegativeSafeInteger))) &&
(hitCount === undefined || isNonNegativeSafeInteger(hitCount))
);
}
function isNonEmptyStringArray(value: unknown): value is readonly string[] {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every(isNonEmptyString)
);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.length > 0;
}
function isNonNegativeSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function isNonNegativeFiniteNumber(value: unknown): value is number {
return (
typeof value === 'number' && Number.isFinite(value) && value >= 0
);
}
@@ -0,0 +1,234 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import {
type InitialImportDiagnosticCapture,
validateInitialImportDiagnosticArtifacts,
} from './initial-import-diagnostic-artifacts';
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
const HEAP_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',
],
};
const HEAP_SNAPSHOT = {
edges: [0, 0, 0],
nodes: [0, 0, 1, 0, 1, 0],
snapshot: { meta: HEAP_META },
strings: ['', 'root'],
};
const TRACE = {
traceEvents: [{ name: 'initial-import', ph: 'X', ts: 1 }],
};
function capture(directory: string): InitialImportDiagnosticCapture {
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 writeArtifacts(directory: string): Promise<void> {
const artifacts: Readonly<Record<string, unknown>> = {
'database.worker-4.cpuprofile': CPU_PROFILE,
'database.worker-4.heapsnapshot': HEAP_SNAPSHOT,
'main.cpuprofile': CPU_PROFILE,
'main.heapsnapshot': HEAP_SNAPSHOT,
'renderer.cpuprofile': CPU_PROFILE,
'renderer.heapsnapshot': HEAP_SNAPSHOT,
'renderer.trace.json': TRACE,
};
await Promise.all(
Object.entries(artifacts).map(([filename, value]) =>
writeFile(join(directory, filename), JSON.stringify(value))
)
);
}
async function withArtifacts(
run: (directory: string) => Promise<void>
): Promise<void> {
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-content-')
);
try {
await writeArtifacts(directory);
await run(directory);
} finally {
await rm(directory, { force: true, recursive: true });
}
}
test('accepts bounded signed CPU time deltas emitted by Chromium', async () => {
await withArtifacts(async (directory) => {
await writeFile(
join(directory, 'renderer.cpuprofile'),
JSON.stringify({
...CPU_PROFILE,
endTime: 5_000,
samples: [1, 1, 1],
timeDeltas: [1_000, -100, 1_000],
})
);
await assert.doesNotReject(() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
)
);
});
});
test('rejects CPU profiles without useful bounded samples and stack references', async () => {
await withArtifacts(async (directory) => {
const invalidProfiles = [
{ ...CPU_PROFILE, samples: [], timeDeltas: [] },
{ ...CPU_PROFILE, timeDeltas: undefined },
{ ...CPU_PROFILE, timeDeltas: [] },
{
...CPU_PROFILE,
endTime: CPU_PROFILE.startTime,
},
{
...CPU_PROFILE,
samples: [2],
},
{
...CPU_PROFILE,
timeDeltas: [-1],
},
{
...CPU_PROFILE,
timeDeltas: [2_001],
},
{
...CPU_PROFILE,
nodes: [
...CPU_PROFILE.nodes,
{ ...CPU_PROFILE.nodes[0], id: 1 },
],
},
{
...CPU_PROFILE,
nodes: [{ id: 1 }],
},
];
for (const profile of invalidProfiles) {
await writeFile(
join(directory, 'main.cpuprofile'),
JSON.stringify(profile)
);
await assert.rejects(
() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
),
/initial-import-diagnostic-artifact-invalid:main-cpu-profile/
);
}
});
});
test('rejects empty or malformed heap tables and invalid field strides', async () => {
await withArtifacts(async (directory) => {
const invalidSnapshots = [
{ snapshot: {}, nodes: [], edges: [], strings: [] },
{ ...HEAP_SNAPSHOT, nodes: [] },
{ ...HEAP_SNAPSHOT, edges: [] },
{ ...HEAP_SNAPSHOT, strings: [] },
{
...HEAP_SNAPSHOT,
snapshot: {
meta: { ...HEAP_META, node_types: undefined },
},
},
{ ...HEAP_SNAPSHOT, nodes: [...HEAP_SNAPSHOT.nodes, 0] },
{ ...HEAP_SNAPSHOT, edges: [...HEAP_SNAPSHOT.edges, 0] },
];
for (const snapshot of invalidSnapshots) {
await writeFile(
join(directory, 'main.heapsnapshot'),
JSON.stringify(snapshot)
);
await assert.rejects(
() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
),
/initial-import-diagnostic-artifact-invalid:main-heap-snapshot/
);
}
});
});
test('rejects empty traces and malformed trace events', async () => {
await withArtifacts(async (directory) => {
const invalidTraces = [
{ traceEvents: [] },
{ traceEvents: [null] },
{ traceEvents: [{ name: '', ph: 'X', ts: 1 }] },
{ traceEvents: [{ name: 'initial-import', ph: '', ts: 1 }] },
{
traceEvents: [
{ name: 'initial-import', ph: 'X', ts: 'invalid' },
],
},
];
for (const trace of invalidTraces) {
await writeFile(
join(directory, 'renderer.trace.json'),
JSON.stringify(trace)
);
await assert.rejects(
() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
),
/initial-import-diagnostic-artifact-invalid:renderer-trace/
);
}
});
});
@@ -0,0 +1,394 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { mkdtemp, rm, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
interface DiagnosticCapture {
readonly main: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly workers: readonly {
readonly kind: string;
readonly ordinal: number;
readonly profilePath: string | null;
readonly snapshotPath: string | null;
}[];
};
readonly renderer: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly tracePath: string | null;
};
}
interface DiagnosticArtifactsModule {
validateInitialImportDiagnosticArtifacts?: (
capture: DiagnosticCapture,
iterationDirectory: string
) => Promise<void>;
}
const diagnosticArtifactsModulePromise = import(
new URL('./initial-import-diagnostic-artifacts.ts', import.meta.url).href
)
.then((module) => module as DiagnosticArtifactsModule)
.catch(() => null);
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: 'initial-import', ph: 'X', ts: 1 }],
};
function capture(directory: string, databaseOrdinal = 4): DiagnosticCapture {
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 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)
),
]);
}
test('accepts the complete parseable initial-import diagnostic artifact set', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module, 'initial-import diagnostic validator must exist');
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-diagnostic-')
);
try {
await writeValidArtifacts(directory);
await assert.doesNotReject(() =>
validate?.(capture(directory), directory)
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('fails closed for malformed or missing required process artifacts', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-invalid-')
);
try {
await writeValidArtifacts(directory);
await writeFile(join(directory, 'renderer.trace.json'), '{');
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:renderer-trace/
);
await writeFile(
join(directory, 'renderer.trace.json'),
JSON.stringify(RENDERER_TRACE)
);
const missingMainProfile: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
cpuProfilePath: null,
},
};
await assert.rejects(
() => validate?.(missingMainProfile, directory),
/initial-import-diagnostic-artifact-missing:main-cpu-profile/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('rejects escaped paths and symlinks instead of reading outside the iteration directory', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-contained-')
);
const outsideDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-outside-')
);
try {
await writeValidArtifacts(directory);
const escapedCapture: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
cpuProfilePath: join(outsideDirectory, 'main.cpuprofile'),
},
};
await writeFile(
join(outsideDirectory, 'main.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(escapedCapture, directory),
/initial-import-diagnostic-artifact-path-invalid:main-cpu-profile/
);
const rendererProfilePath = join(directory, 'renderer.cpuprofile');
await unlink(rendererProfilePath);
await symlink(
join(outsideDirectory, 'main.cpuprofile'),
rendererProfilePath
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:renderer-cpu-profile/
);
} finally {
await rm(directory, { force: true, recursive: true });
await rm(outsideDirectory, { force: true, recursive: true });
}
});
test('requires exactly one database profile and rejects every playlist-worker profile', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-workers-')
);
try {
await writeValidArtifacts(directory);
await writeFile(
join(directory, 'database.worker-9.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-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(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-playlist-profile-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('requires a structurally valid database worker heap snapshot', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-')
);
try {
await writeValidArtifacts(directory);
const missingSnapshot: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
workers: [
{
...capture(directory).main.workers[0],
snapshotPath: null,
},
],
},
};
await assert.rejects(
() => validate?.(missingSnapshot, directory),
/initial-import-diagnostic-artifact-missing:database-heap-snapshot/
);
await writeFile(
join(directory, 'database.worker-4.heapsnapshot'),
JSON.stringify({ snapshot: {}, nodes: [], edges: [] })
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:database-heap-snapshot/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('rejects escaped and symlinked database worker heap snapshots', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-contained-')
);
const outsideDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-outside-')
);
try {
await writeValidArtifacts(directory);
const outsideSnapshotPath = join(
outsideDirectory,
'database.worker-4.heapsnapshot'
);
await writeFile(outsideSnapshotPath, JSON.stringify(HEAP_SNAPSHOT));
const escapedSnapshot: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
workers: [
{
...capture(directory).main.workers[0],
snapshotPath: outsideSnapshotPath,
},
],
},
};
await assert.rejects(
() => validate?.(escapedSnapshot, directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
const snapshotPath = join(directory, 'database.worker-4.heapsnapshot');
await unlink(snapshotPath);
await symlink(outsideSnapshotPath, snapshotPath);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
} finally {
await rm(directory, { force: true, recursive: true });
await rm(outsideDirectory, { force: true, recursive: true });
}
});
test('rejects extra database snapshots and every playlist-worker snapshot', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-count-')
);
try {
await writeValidArtifacts(directory);
await writeFile(
join(directory, 'database.worker-9.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-database-snapshot-cardinality-invalid/
);
await unlink(join(directory, 'database.worker-9.heapsnapshot'));
await writeFile(
join(directory, 'playlist-refresh.worker-2.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-playlist-snapshot-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
@@ -0,0 +1,262 @@
import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import {
isCpuProfile,
isHeapSnapshot,
isRendererTrace,
} from './initial-import-diagnostic-artifact-content-validation';
interface DiagnosticWorkerArtifact {
readonly kind: string;
readonly ordinal: number;
readonly profilePath: string | null;
readonly snapshotPath: string | null;
}
export interface InitialImportDiagnosticCapture {
readonly main: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly workers: readonly DiagnosticWorkerArtifact[];
};
readonly renderer: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly tracePath: string | null;
};
}
type ArtifactLabel =
| 'database-cpu-profile'
| 'database-heap-snapshot'
| 'main-cpu-profile'
| 'main-heap-snapshot'
| 'renderer-cpu-profile'
| 'renderer-heap-snapshot'
| 'renderer-trace';
interface ArtifactDescription {
readonly filename: string;
readonly label: ArtifactLabel;
readonly path: string | null;
readonly validate: (value: unknown) => boolean;
}
export async function validateInitialImportDiagnosticArtifacts(
capture: InitialImportDiagnosticCapture,
iterationDirectory: string
): Promise<void> {
const directory = resolve(iterationDirectory);
let realDirectory: string;
try {
realDirectory = await realpath(directory);
} catch {
throw new Error(
'initial-import-diagnostic-artifact-path-invalid:iteration-directory'
);
}
const databaseWorker = requireExactDatabaseWorker(capture.main.workers);
await assertWorkerArtifactCardinality(directory);
const artifacts: readonly ArtifactDescription[] = [
{
filename: 'main.cpuprofile',
label: 'main-cpu-profile',
path: capture.main.cpuProfilePath,
validate: isCpuProfile,
},
{
filename: 'renderer.cpuprofile',
label: 'renderer-cpu-profile',
path: capture.renderer.cpuProfilePath,
validate: isCpuProfile,
},
{
filename: 'renderer.trace.json',
label: 'renderer-trace',
path: capture.renderer.tracePath,
validate: isRendererTrace,
},
{
filename: 'main.heapsnapshot',
label: 'main-heap-snapshot',
path: capture.main.heapSnapshotPath,
validate: isHeapSnapshot,
},
{
filename: 'renderer.heapsnapshot',
label: 'renderer-heap-snapshot',
path: capture.renderer.heapSnapshotPath,
validate: isHeapSnapshot,
},
{
filename: `database.worker-${databaseWorker.ordinal}.cpuprofile`,
label: 'database-cpu-profile',
path: databaseWorker.profilePath,
validate: isCpuProfile,
},
{
filename: `database.worker-${databaseWorker.ordinal}.heapsnapshot`,
label: 'database-heap-snapshot',
path: databaseWorker.snapshotPath,
validate: isHeapSnapshot,
},
];
for (const artifact of artifacts) {
await validateJsonArtifact(directory, realDirectory, artifact);
}
}
function requireExactDatabaseWorker(
workers: readonly DiagnosticWorkerArtifact[]
): DiagnosticWorkerArtifact {
if (
workers.some(
(worker) =>
worker.kind === 'playlist-refresh.worker' &&
worker.profilePath !== null
)
) {
throw new Error(
'initial-import-diagnostic-playlist-profile-unexpected'
);
}
if (
workers.some(
(worker) =>
worker.kind === 'playlist-refresh.worker' &&
worker.snapshotPath !== null
)
) {
throw new Error(
'initial-import-diagnostic-playlist-snapshot-unexpected'
);
}
const databaseWorkers = workers.filter(
(worker) => worker.kind === 'database.worker'
);
const worker = databaseWorkers[0];
if (
databaseWorkers.length !== 1 ||
!worker ||
!Number.isSafeInteger(worker.ordinal) ||
worker.ordinal < 0
) {
throw new Error(
'initial-import-diagnostic-database-profile-cardinality-invalid'
);
}
return worker;
}
async function assertWorkerArtifactCardinality(
directory: string
): Promise<void> {
let entries: readonly string[];
try {
entries = await readdir(directory);
} catch {
throw new Error(
'initial-import-diagnostic-artifact-path-invalid:iteration-directory'
);
}
if (
entries.some((entry) =>
/^playlist-refresh\.worker-\d+\.cpuprofile$/u.test(entry)
)
) {
throw new Error(
'initial-import-diagnostic-playlist-profile-unexpected'
);
}
if (
entries.filter((entry) =>
/^database\.worker-\d+\.cpuprofile$/u.test(entry)
).length !== 1
) {
throw new Error(
'initial-import-diagnostic-database-profile-cardinality-invalid'
);
}
if (
entries.some((entry) =>
/^playlist-refresh\.worker-\d+\.heapsnapshot$/u.test(entry)
)
) {
throw new Error(
'initial-import-diagnostic-playlist-snapshot-unexpected'
);
}
if (
entries.filter((entry) =>
/^database\.worker-\d+\.heapsnapshot$/u.test(entry)
).length !== 1
) {
throw new Error(
'initial-import-diagnostic-database-snapshot-cardinality-invalid'
);
}
}
async function validateJsonArtifact(
directory: string,
realDirectory: string,
artifact: ArtifactDescription
): Promise<void> {
if (artifact.path === null) {
throw new Error(
`initial-import-diagnostic-artifact-missing:${artifact.label}`
);
}
const expectedPath = join(directory, artifact.filename);
if (resolve(artifact.path) !== expectedPath) {
throw new Error(
`initial-import-diagnostic-artifact-path-invalid:${artifact.label}`
);
}
let value: unknown;
try {
const stats = await lstat(expectedPath);
if (!stats.isFile() || stats.isSymbolicLink()) {
throw new Error('unsafe-artifact-type');
}
const realArtifactPath = await realpath(expectedPath);
if (!isContained(realDirectory, realArtifactPath)) {
throw new Error('escaped-artifact');
}
value = JSON.parse(await readFile(realArtifactPath, 'utf8')) as unknown;
} catch (error) {
if (
error instanceof Error &&
(error.message === 'unsafe-artifact-type' ||
error.message === 'escaped-artifact')
) {
throw new Error(
`initial-import-diagnostic-artifact-path-invalid:${artifact.label}`
);
}
throw new Error(
`initial-import-diagnostic-artifact-invalid:${artifact.label}`
);
}
if (!artifact.validate(value)) {
throw new Error(
`initial-import-diagnostic-artifact-invalid:${artifact.label}`
);
}
}
function isContained(directory: string, artifactPath: string): boolean {
const relativePath = relative(directory, artifactPath);
return (
relativePath.length > 0 &&
!isAbsolute(relativePath) &&
relativePath !== '..' &&
!relativePath.startsWith(`..${sep}`)
);
}
@@ -0,0 +1,19 @@
export const CPU_PROFILE = Object.freeze({
endTime: 3_000,
nodes: Object.freeze([
Object.freeze({
callFrame: Object.freeze({
columnNumber: -1,
functionName: '(root)',
lineNumber: -1,
scriptId: '0',
url: '',
}),
hitCount: 1,
id: 1,
}),
]),
samples: Object.freeze([1]),
startTime: 1_000,
timeDeltas: Object.freeze([1_000]),
});
@@ -0,0 +1,115 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { PERFORMANCE_ITERATION_KIND } from './m3u-refresh-cancellation-contract';
import {
assertSyntheticM3uImportUrl,
createM3uImportIterationDefinitions,
listM3uImportScenarios,
M3U_IMPORT_SCENARIO_ID,
} from './m3u-import-benchmark-contract';
describe('formal M3U import benchmark contract', () => {
it('defines the exact 10k, 50k, and 100k synthetic scenarios', () => {
assert.deepEqual(listM3uImportScenarios(), [
{
channelCount: 10_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_10K,
},
{
channelCount: 50_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_50K,
},
{
channelCount: 100_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_100K,
},
]);
});
it('schedules one warm-up, five measured runs, and one diagnostic per size', () => {
const definitions = createM3uImportIterationDefinitions(false);
assert.equal(definitions.length, 21);
for (const scenario of listM3uImportScenarios()) {
const scenarioRuns = definitions.filter(
(definition) => definition.scenarioId === scenario.id
);
assert.deepEqual(
scenarioRuns.map(({ kind, runId }) => ({ kind, runId })),
[
{
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
runId: 'warmup-01',
},
...Array.from({ length: 5 }, (_, index) => ({
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
runId: `run-${String(index + 1).padStart(2, '0')}`,
})),
{
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
runId: 'diagnostic',
},
]
);
assert.ok(
scenarioRuns.every(
(definition) =>
definition.channelCount === scenario.channelCount
)
);
}
});
it('keeps fixture sizes unchanged while smoke mode reduces measured runs to one', () => {
const definitions = createM3uImportIterationDefinitions(true);
assert.equal(definitions.length, 9);
for (const scenario of listM3uImportScenarios()) {
assert.deepEqual(
definitions
.filter(
(definition) => definition.scenarioId === scenario.id
)
.map(({ kind, runId }) => ({ kind, runId })),
[
{
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
runId: 'warmup-01',
},
{
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
runId: 'run-01',
},
{
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
runId: 'diagnostic',
},
]
);
}
});
it('accepts only the exact credential-free loopback fixture URL', () => {
assert.doesNotThrow(() =>
assertSyntheticM3uImportUrl(
'http://127.0.0.1:43210/synthetic-performance.m3u'
)
);
for (const unsafe of [
'https://127.0.0.1:43210/synthetic-performance.m3u',
'http://localhost:43210/synthetic-performance.m3u',
'http://127.0.0.1/synthetic-performance.m3u',
'http://user:pass@127.0.0.1:43210/synthetic-performance.m3u',
'http://127.0.0.1:43210/other.m3u',
'http://127.0.0.1:43210/synthetic-performance.m3u?token=secret',
'http://127.0.0.1:43210/synthetic-performance.m3u#fragment',
]) {
assert.throws(
() => assertSyntheticM3uImportUrl(unsafe),
/exact synthetic HTTP loopback URL/
);
}
});
});
@@ -0,0 +1,118 @@
import {
PERFORMANCE_ITERATION_KIND,
type PerformanceIterationKind,
} from './m3u-refresh-cancellation-contract';
import { SYNTHETIC_M3U_RESOURCE_PATH } from './synthetic-m3u-server';
import type { SyntheticM3uChannelCount } from './synthetic-m3u';
export const M3U_IMPORT_SCENARIO_ID = {
IMPORT_10K: 'm3u-import-10k',
IMPORT_50K: 'm3u-import-50k',
IMPORT_100K: 'm3u-import-100k',
} as const;
export type M3uImportScenarioId =
(typeof M3U_IMPORT_SCENARIO_ID)[keyof typeof M3U_IMPORT_SCENARIO_ID];
export interface M3uImportScenario {
readonly channelCount: SyntheticM3uChannelCount;
readonly id: M3uImportScenarioId;
}
export interface M3uImportIterationDefinition {
readonly channelCount: SyntheticM3uChannelCount;
readonly kind: PerformanceIterationKind;
readonly runId: string;
readonly scenarioId: M3uImportScenarioId;
}
const SCENARIOS: readonly M3uImportScenario[] = Object.freeze([
Object.freeze({
channelCount: 10_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_10K,
}),
Object.freeze({
channelCount: 50_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_50K,
}),
Object.freeze({
channelCount: 100_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_100K,
}),
]);
export function listM3uImportScenarios(): readonly M3uImportScenario[] {
return SCENARIOS;
}
export function createM3uImportIterationDefinitions(
smoke: boolean
): readonly M3uImportIterationDefinition[] {
const measuredRuns = smoke ? 1 : 5;
const definitions: M3uImportIterationDefinition[] = [];
for (const scenario of SCENARIOS) {
definitions.push(
iteration(scenario, PERFORMANCE_ITERATION_KIND.WARMUP, 'warmup-01')
);
for (let index = 1; index <= measuredRuns; index += 1) {
definitions.push(
iteration(
scenario,
PERFORMANCE_ITERATION_KIND.MEASURED,
`run-${String(index).padStart(2, '0')}`
)
);
}
definitions.push(
iteration(
scenario,
PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
'diagnostic'
)
);
}
return Object.freeze(definitions);
}
export function assertSyntheticM3uImportUrl(value: string): void {
let url: URL;
try {
url = new URL(value);
} catch {
throw invalidSyntheticUrl();
}
if (
url.protocol !== 'http:' ||
url.hostname !== '127.0.0.1' ||
url.port.length === 0 ||
url.username.length > 0 ||
url.password.length > 0 ||
url.pathname !== SYNTHETIC_M3U_RESOURCE_PATH ||
url.search.length > 0 ||
url.hash.length > 0
) {
throw invalidSyntheticUrl();
}
}
function iteration(
scenario: M3uImportScenario,
kind: PerformanceIterationKind,
runId: string
): M3uImportIterationDefinition {
return Object.freeze({
channelCount: scenario.channelCount,
kind,
runId,
scenarioId: scenario.id,
});
}
function invalidSyntheticUrl(): Error {
return new Error(
'M3U performance fixture must use the exact synthetic HTTP loopback URL'
);
}
@@ -0,0 +1,184 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { describe, it } from 'node:test';
import {
assertM3uImportOutputPathHasNoSymlinks,
assertM3uImportSourceState,
M3U_IMPORT_RENDERER_CDP_PORT,
resolveM3uImportRendererCdpPort,
resolveM3uImportOutputLayout,
} from './m3u-import-benchmark-support';
import { assertM3uImportRequestDelta } from './m3u-import.benchmark';
describe('initial M3U import benchmark lifecycle', () => {
it('keeps requested output and variant strictly below dist/performance', () => {
const workspaceRoot = resolve('/workspace/iptvnator');
const outputRoot = resolve(
workspaceRoot,
'dist/performance/20260727T040000Z-m3u-import'
);
assert.deepEqual(
resolveM3uImportOutputLayout(workspaceRoot, outputRoot, 'baseline'),
{
outputRoot,
variantDirectory: resolve(outputRoot, 'baseline'),
}
);
for (const requestedRoot of [
'dist/performance/run',
resolve(workspaceRoot, 'dist/performance'),
resolve(workspaceRoot, 'dist/performance-sibling/run'),
resolve(workspaceRoot, '../outside'),
]) {
assert.throws(
() =>
resolveM3uImportOutputLayout(
workspaceRoot,
requestedRoot,
'baseline'
),
/absolute path below dist\/performance/
);
}
assert.throws(
() =>
resolveM3uImportOutputLayout(
workspaceRoot,
outputRoot,
'../escape'
),
/variant is invalid/i
);
});
it('requires a clean source tree for formal runs but permits dirty smoke evidence', () => {
const dirtyState = {
commit: 'a'.repeat(40),
diffSha256: 'b'.repeat(64),
dirty: true,
};
assert.throws(
() => assertM3uImportSourceState(false, dirtyState),
/Formal performance runs require a clean Git worktree/
);
assert.doesNotThrow(() => assertM3uImportSourceState(true, dirtyState));
assert.doesNotThrow(() =>
assertM3uImportSourceState(false, {
...dirtyState,
dirty: false,
})
);
});
it('rejects an existing output ancestor symlink before artifact creation', async () => {
const fixtureRoot = await mkdtemp(
join(tmpdir(), 'iptvnator-m3u-output-path-')
);
const repositoryRoot = join(fixtureRoot, 'repository');
const performanceRoot = join(
repositoryRoot,
'dist',
'performance'
);
const outside = join(fixtureRoot, 'outside');
await mkdir(performanceRoot, { recursive: true });
await mkdir(outside);
await symlink(outside, join(performanceRoot, 'redirect'));
try {
await assert.rejects(
assertM3uImportOutputPathHasNoSymlinks(
repositoryRoot,
join(performanceRoot, 'redirect', 'run')
),
/Performance output path contains a symbolic link/
);
await assert.doesNotReject(
assertM3uImportOutputPathHasNoSymlinks(
repositoryRoot,
join(performanceRoot, 'ordinary', 'run')
)
);
} finally {
await rm(fixtureRoot, { force: true, recursive: true });
}
});
it('keeps formal CDP capture on 9222 while allowing an isolated smoke port', () => {
assert.equal(
resolveM3uImportRendererCdpPort(false, undefined),
M3U_IMPORT_RENDERER_CDP_PORT
);
assert.equal(
resolveM3uImportRendererCdpPort(false, '9222'),
M3U_IMPORT_RENDERER_CDP_PORT
);
assert.equal(resolveM3uImportRendererCdpPort(true, '9322'), 9322);
assert.throws(
() => resolveM3uImportRendererCdpPort(false, '9322'),
/Formal performance runs require CDP port 9222/
);
for (const invalid of ['0', '1023', '65536', '9222.5', 'not-a-port']) {
assert.throws(
() => resolveM3uImportRendererCdpPort(true, invalid),
/IPTVNATOR_PERF_CDP_PORT is invalid/
);
}
});
it('fails closed unless one and only one app GET occurs', () => {
assert.doesNotThrow(() => assertM3uImportRequestDelta(3, 4));
assert.throws(
() => assertM3uImportRequestDelta(3, 3),
/exactly one application GET/
);
assert.throws(
() => assertM3uImportRequestDelta(3, 5),
/exactly one application GET/
);
});
it('waits for the exact upsert plus route-reload GET sequence', () => {
const source = readFileSync(
resolve(
process.cwd(),
'src/performance/m3u-import.benchmark.ts'
),
'utf8'
);
assert.match(source, /status\.databaseRequests\s*===\s*2/);
assert.match(source, /status\.databaseUpsertsCompleted\s*===\s*1/);
assert.match(source, /status\.databaseGetsCompleted\s*===\s*1/);
assert.match(source, /status\.databasePending\s*===\s*0/);
assert.match(source, /status\.preloadSuccessMarkers\s*===\s*2/);
});
it('always disposes renderer capture before closing Electron on iteration failure', () => {
const source = readFileSync(
resolve(
process.cwd(),
'src/performance/m3u-import.benchmark.ts'
),
'utf8'
);
const runIteration = source.indexOf('async function runIteration');
const finallyBlock = source.indexOf('} finally {', runIteration);
const rendererDispose = source.indexOf(
'await renderer.dispose()',
finallyBlock
);
const appClose = source.indexOf('await closeElectronApp(app)', finallyBlock);
assert.ok(finallyBlock > runIteration);
assert.ok(rendererDispose > finallyBlock);
assert.ok(appClose > rendererDispose);
});
});
@@ -0,0 +1,300 @@
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { access, lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
import { createServer as createTcpServer } from 'node:net';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import { expect, type Page } from '@playwright/test';
import {
type LaunchedElectronApp,
workspaceRoot,
} from '../electron-test-fixtures';
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
import type { RendererWindowIdentity } from './renderer-window-rss-session';
const OUTPUT_VARIANT_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
export const M3U_IMPORT_RENDERER_CDP_PORT = 9222;
export interface M3uImportGitSourceState {
readonly commit: string;
readonly diffSha256: string;
readonly dirty: boolean;
}
export interface M3uImportOutputLayout {
readonly outputRoot: string;
readonly variantDirectory: string;
}
export interface M3uImportBenchmarkConfiguration extends M3uImportOutputLayout {
readonly electronVersion: string;
readonly measuredRuns: number;
readonly rendererCdpPort: number;
readonly smoke: boolean;
readonly sourceState: M3uImportGitSourceState;
readonly variant: string;
}
export async function resolveM3uImportBenchmarkConfiguration(): Promise<M3uImportBenchmarkConfiguration> {
const requestedRoot = process.env['IPTVNATOR_PERF_OUTPUT_DIR'];
if (!requestedRoot) {
throw new Error('IPTVNATOR_PERF_OUTPUT_DIR is required');
}
const variant = process.env['IPTVNATOR_PERF_VARIANT'] ?? 'baseline';
const layout = resolveM3uImportOutputLayout(
workspaceRoot,
requestedRoot,
variant
);
const smoke = process.env['IPTVNATOR_PERF_SMOKE'] === '1';
const rendererCdpPort = resolveM3uImportRendererCdpPort(
smoke,
process.env['IPTVNATOR_PERF_CDP_PORT']
);
const sourceState = readGitSourceState(workspaceRoot);
assertM3uImportSourceState(smoke, sourceState);
const electronPackage = JSON.parse(
await readFile(
join(workspaceRoot, 'node_modules', 'electron', 'package.json'),
'utf8'
)
) as { version?: unknown };
if (typeof electronPackage.version !== 'string') {
throw new Error('Unable to resolve the Electron runtime version');
}
await assertM3uImportOutputPathHasNoSymlinks(
workspaceRoot,
layout.outputRoot
);
await assertMissing(layout.variantDirectory);
await assertPerformanceArtifactCapacity(layout.variantDirectory);
await mkdir(layout.variantDirectory, { recursive: true });
return Object.freeze({
...layout,
electronVersion: electronPackage.version,
measuredRuns: smoke ? 1 : 5,
rendererCdpPort,
smoke,
sourceState,
variant,
});
}
export function resolveM3uImportOutputLayout(
repositoryRoot: string,
requestedRoot: string,
variant: string
): M3uImportOutputLayout {
const performanceRoot = resolve(repositoryRoot, 'dist', 'performance');
const outputRoot = resolve(requestedRoot);
if (
!isAbsolute(requestedRoot) ||
!isStrictDescendant(performanceRoot, outputRoot)
) {
throw new Error(
'Performance output must be an absolute path below dist/performance'
);
}
if (!OUTPUT_VARIANT_PATTERN.test(variant)) {
throw new Error('IPTVNATOR_PERF_VARIANT is invalid');
}
const variantDirectory = resolve(outputRoot, variant);
if (!isStrictDescendant(outputRoot, variantDirectory)) {
throw new Error('IPTVNATOR_PERF_VARIANT is invalid');
}
return Object.freeze({ outputRoot, variantDirectory });
}
export async function assertM3uImportOutputPathHasNoSymlinks(
repositoryRoot: string,
outputRoot: string
): Promise<void> {
const root = resolve(repositoryRoot);
const output = resolve(outputRoot);
if (!isStrictDescendant(root, output)) {
throw new Error('Performance output path escapes the repository');
}
const components = relative(root, output).split(sep);
let current = root;
for (const component of ['', ...components]) {
current = component === '' ? current : join(current, component);
try {
const stats = await lstat(current);
if (stats.isSymbolicLink()) {
throw new Error(
`Performance output path contains a symbolic link: ${current}`
);
}
} catch (error) {
if (isMissingPath(error)) {
return;
}
throw error;
}
}
}
export function assertM3uImportSourceState(
smoke: boolean,
state: M3uImportGitSourceState
): void {
if (!smoke && state.dirty) {
throw new Error(
'Formal performance runs require a clean Git worktree; commit or stash changes first'
);
}
}
export function resolveM3uImportRendererCdpPort(
smoke: boolean,
requestedPort: string | undefined
): number {
const port =
requestedPort === undefined
? M3U_IMPORT_RENDERER_CDP_PORT
: Number(requestedPort);
if (
(requestedPort !== undefined && !/^[1-9]\d*$/.test(requestedPort)) ||
!Number.isSafeInteger(port) ||
port < 1024 ||
port > 65_535
) {
throw new Error('IPTVNATOR_PERF_CDP_PORT is invalid');
}
if (!smoke && port !== M3U_IMPORT_RENDERER_CDP_PORT) {
throw new Error(
`Formal performance runs require CDP port ${M3U_IMPORT_RENDERER_CDP_PORT}`
);
}
return port;
}
export async function resolveRendererWindowIdentity(
app: LaunchedElectronApp
): Promise<RendererWindowIdentity> {
const handle = await app.electronApp.browserWindow(app.mainWindow);
try {
return await handle.evaluate((browserWindow) => {
const exactWindow = browserWindow as unknown as {
readonly id: number;
readonly webContents: { readonly id: number };
};
return {
browserWindowId: exactWindow.id,
webContentsId: exactWindow.webContents.id,
};
});
} finally {
await handle.dispose();
}
}
export async function assertTcpPortAvailable(port: number): Promise<void> {
const server = createTcpServer();
await new Promise<void>((resolvePromise, rejectPromise) => {
server.once('error', rejectPromise);
server.listen(
{ exclusive: true, host: '127.0.0.1', port },
resolvePromise
);
});
await new Promise<void>((resolvePromise, rejectPromise) => {
server.close((error) =>
error ? rejectPromise(error) : resolvePromise()
);
});
}
export async function assertRendererCdpTarget(
page: Page,
port: number
): Promise<void> {
await expect
.poll(
async () => {
try {
const response = await fetch(
`http://127.0.0.1:${port}/json/list`
);
const targets = response.ok
? ((await response.json()) as unknown)
: null;
return (
Array.isArray(targets) &&
targets.some(
(target) =>
typeof target === 'object' &&
target !== null &&
(target as Record<string, unknown>)['type'] ===
'page' &&
(target as Record<string, unknown>)['url'] ===
page.url()
)
);
} catch {
return false;
}
},
{ timeout: 10_000 }
)
.toBe(true);
}
export async function writeM3uImportJson(
path: string,
value: unknown
): Promise<void> {
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function readGitSourceState(repositoryRoot: string): M3uImportGitSourceState {
const git = (args: readonly string[]): string =>
execFileSync('git', [...args], {
cwd: repositoryRoot,
encoding: 'utf8',
});
const commit = git(['rev-parse', 'HEAD']).trim();
const status = git(['status', '--porcelain=v1', '--untracked-files=all']);
const diffSha256 = createHash('sha256')
.update(status)
.update('\0')
.update(git(['diff', '--binary', 'HEAD']))
.update('\0')
.update(git(['diff', '--binary', '--cached', 'HEAD']))
.digest('hex');
return Object.freeze({
commit,
diffSha256,
dirty: status.trim().length > 0,
});
}
function isStrictDescendant(parent: string, candidate: string): boolean {
const pathFromParent = relative(resolve(parent), resolve(candidate));
return (
pathFromParent.length > 0 &&
pathFromParent !== '..' &&
!pathFromParent.startsWith(`..${sep}`) &&
!isAbsolute(pathFromParent)
);
}
async function assertMissing(path: string): Promise<void> {
try {
await access(path);
} catch {
return;
}
throw new Error(`Performance output already exists: ${path}`);
}
function isMissingPath(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
error.code === 'ENOENT'
);
}
@@ -0,0 +1,250 @@
import type {
MainTimelineRecord,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
type InitialImportDatabaseOperation =
| 'DB_GET_APP_PLAYLIST'
| 'DB_UPSERT_APP_PLAYLIST';
export interface M3uImportIpcAttribution {
readonly databaseWorkerToMainCloneProxyMs: number;
readonly ipcStructuredCloneProxyMs: number;
readonly mainToDatabaseWorkerCloneProxyMs: number;
readonly mainToRendererCloneProxyMs: number;
readonly rendererToMainCloneProxyMs: number;
}
interface RequestBoundaryAttribution {
readonly mainToDatabaseWorkerMs: number;
readonly mainToRendererMs: number;
readonly rendererToMainMs: number;
readonly successEpochMs: number;
readonly workerToMainMs: number;
}
export function deriveM3uImportIpcAttribution(input: {
readonly get: WorkerRequestPerformanceMetrics;
readonly getReadStartedEpochMs: number;
readonly importDispatchEndedEpochMs: number;
readonly importDispatchStartedEpochMs: number;
readonly normalizeEndedEpochMs: number;
readonly playlistId: string;
readonly publishChannelsStartedEpochMs: number;
readonly serializeStartedEpochMs: number;
readonly timeline: readonly MainTimelineRecord[];
readonly upsert: WorkerRequestPerformanceMetrics;
}): M3uImportIpcAttribution {
requireOrdered(
input.normalizeEndedEpochMs,
input.importDispatchStartedEpochMs
);
requireOrdered(
input.importDispatchStartedEpochMs,
input.importDispatchEndedEpochMs
);
const upsert = deriveRequestBoundaryAttribution({
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: input.playlistId,
request: input.upsert,
timeline: input.timeline,
workPhaseStartedEpochMs: input.serializeStartedEpochMs,
});
requireOrdered(input.importDispatchEndedEpochMs, requireSource(input.upsert));
const get = deriveRequestBoundaryAttribution({
operation: 'DB_GET_APP_PLAYLIST',
playlistId: input.playlistId,
request: input.get,
timeline: input.timeline,
workPhaseStartedEpochMs: input.getReadStartedEpochMs,
});
requireOrdered(upsert.successEpochMs, requireSource(input.get));
requireOrdered(
get.successEpochMs,
input.publishChannelsStartedEpochMs
);
const initialMainToRendererMs = difference(
input.importDispatchStartedEpochMs,
input.normalizeEndedEpochMs
);
const mainToRendererCloneProxyMs =
initialMainToRendererMs +
upsert.mainToRendererMs +
get.mainToRendererMs;
const rendererToMainCloneProxyMs =
upsert.rendererToMainMs + get.rendererToMainMs;
const mainToDatabaseWorkerCloneProxyMs =
upsert.mainToDatabaseWorkerMs + get.mainToDatabaseWorkerMs;
const databaseWorkerToMainCloneProxyMs =
upsert.workerToMainMs + get.workerToMainMs;
return Object.freeze({
databaseWorkerToMainCloneProxyMs,
ipcStructuredCloneProxyMs:
mainToRendererCloneProxyMs +
rendererToMainCloneProxyMs +
mainToDatabaseWorkerCloneProxyMs +
databaseWorkerToMainCloneProxyMs,
mainToDatabaseWorkerCloneProxyMs,
mainToRendererCloneProxyMs,
rendererToMainCloneProxyMs,
});
}
function deriveRequestBoundaryAttribution(input: {
readonly operation: InitialImportDatabaseOperation;
readonly playlistId: string;
readonly request: WorkerRequestPerformanceMetrics;
readonly timeline: readonly MainTimelineRecord[];
readonly workPhaseStartedEpochMs: number;
}): RequestBoundaryAttribution {
requireInitialImportPreloadIdentity(input.request, input.operation);
const requestTimeline = requireTimelineRecord(
input.timeline,
'db-request',
input.request,
input.operation,
input.playlistId
);
requireTimelineRecord(
input.timeline,
'db-response',
input.request,
input.operation,
input.playlistId
);
const successTimeline = requireTimelineRecord(
input.timeline,
'preload-performance-success',
input.request,
input.operation,
input.playlistId
);
const sourceEpochMs = requireSource(input.request);
const requestReceivedEpochMs = requireTimestamp(
input.request.requestReceivedEpochMs
);
const workStartedEpochMs = requireTimestamp(
input.request.workStartedEpochMs
);
const workEndedEpochMs = requireTimestamp(input.request.workEndedEpochMs);
const responsePostedEpochMs = requireTimestamp(
input.request.responsePostedEpochMs
);
const responseEpochMs = requireTimestamp(input.request.responseEpochMs);
const successEpochMs = requireTimestamp(successTimeline.sourceEpochMs);
requireOrdered(sourceEpochMs, requestTimeline.epochMs);
requireOrdered(requestTimeline.epochMs, requestReceivedEpochMs);
requireOrdered(requestReceivedEpochMs, workStartedEpochMs);
requireOrdered(workStartedEpochMs, input.workPhaseStartedEpochMs);
requireOrdered(input.workPhaseStartedEpochMs, workEndedEpochMs);
requireOrdered(workEndedEpochMs, responsePostedEpochMs);
requireOrdered(responsePostedEpochMs, responseEpochMs);
requireOrdered(responseEpochMs, successEpochMs);
return {
mainToDatabaseWorkerMs: difference(
requestReceivedEpochMs,
requestTimeline.epochMs
),
mainToRendererMs: difference(successEpochMs, responseEpochMs),
rendererToMainMs: difference(requestTimeline.epochMs, sourceEpochMs),
successEpochMs,
workerToMainMs: difference(responseEpochMs, responsePostedEpochMs),
};
}
function requireTimelineRecord(
timeline: readonly MainTimelineRecord[],
type: 'db-request' | 'db-response' | 'preload-performance-success',
request: WorkerRequestPerformanceMetrics,
operation: InitialImportDatabaseOperation,
playlistId: string
): MainTimelineRecord {
if (!request.requestId || !Number.isSafeInteger(request.ipcCallId)) {
throw new Error('database-request-identity-missing');
}
const matches = timeline.filter((record) => {
if (
record.type !== type ||
record.operation !== operation ||
record.playlistId !== playlistId
) {
return false;
}
if (type === 'preload-performance-success') {
return (
record.ipcCallId === request.ipcCallId &&
record.sourceEpochMs !== undefined &&
record.success === true
);
}
return (
record.requestId === request.requestId &&
(type !== 'db-response' || record.success === true)
);
});
if (matches.length !== 1 || !matches[0]) {
throw new Error(`database-${type}-timeline-correlation`);
}
const record = matches[0];
if (
type === 'db-request' &&
(record.ipcCallId !== request.ipcCallId ||
record.sourceEpochMs !== request.sourceEpochMs)
) {
throw new Error('database-request-timeline-identity');
}
return record;
}
function requireInitialImportPreloadIdentity(
request: WorkerRequestPerformanceMetrics,
operation: InitialImportDatabaseOperation
): void {
if (
request.operation !== operation ||
!request.success ||
request.performanceCaptureUnavailableReason !== null ||
!Number.isSafeInteger(request.ipcCallId) ||
Number(request.ipcCallId) < 1 ||
typeof request.sourceEpochMs !== 'number' ||
!Number.isFinite(request.sourceEpochMs) ||
request.sourceEpochMs < 0 ||
request.operationId !== null ||
request.operationIdUnavailableReason !==
'preload-performance-marker-uncorrelated'
) {
throw new Error('database-request-preload-identity');
}
}
function requireSource(request: WorkerRequestPerformanceMetrics): number {
return requireTimestamp(request.sourceEpochMs);
}
function requireTimestamp(value: number | null | undefined): number {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
throw new Error('database-request-timestamp-missing');
}
return value;
}
function requireOrdered(startEpochMs: number, endEpochMs: number): void {
if (
!Number.isFinite(startEpochMs) ||
!Number.isFinite(endEpochMs) ||
endEpochMs < startEpochMs
) {
throw new Error('phase-order');
}
}
function difference(endEpochMs: number, startEpochMs: number): number {
requireOrdered(startEpochMs, endEpochMs);
return endEpochMs - startEpochMs;
}
@@ -0,0 +1,181 @@
import { once } from 'node:events';
import { createWriteStream } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { CDPSession } from '@playwright/test';
interface TraceData {
readonly value: readonly unknown[];
}
export interface RendererArtifactCapture {
beginTraceOutput(): void;
readonly cpuProfilePath: string | null;
finishTraceOutput(): Promise<void>;
readonly heapSnapshotPath: string | null;
readonly onTraceComplete: () => void;
readonly onTraceData: (data: TraceData) => void;
readonly traceComplete: Promise<void>;
readonly tracePath: string | null;
}
export function createRendererArtifactCapture(options: {
readonly diagnostic: boolean;
readonly outputDirectory: string;
}): RendererArtifactCapture {
const tracePath = options.diagnostic
? join(options.outputDirectory, 'renderer.trace.json')
: null;
const traceOutput = tracePath ? createWriteStream(tracePath) : null;
let traceOutputOpened = false;
let traceOutputFinish: Promise<void> | null = null;
let traceFirstEvent = true;
let completeTrace: (() => void) | null = null;
const traceComplete = new Promise<void>((resolve) => {
completeTrace = resolve;
});
return {
beginTraceOutput: () => {
if (
traceOutput &&
!traceOutputOpened &&
traceOutputFinish === null
) {
traceOutput.write('{"traceEvents":[');
traceOutputOpened = true;
}
},
cpuProfilePath: options.diagnostic
? join(options.outputDirectory, 'renderer.cpuprofile')
: null,
finishTraceOutput: () => {
traceOutputFinish ??= (async () => {
if (!traceOutput) {
return;
}
if (!traceOutputOpened) {
traceOutput.write('{"traceEvents":[');
traceOutputOpened = true;
}
if (!traceOutput.writableEnded) {
traceOutput.write(']}');
traceOutput.end();
}
if (!traceOutput.writableFinished) {
await once(traceOutput, 'finish');
}
})();
return traceOutputFinish;
},
heapSnapshotPath: options.diagnostic
? join(options.outputDirectory, 'renderer.heapsnapshot')
: null,
onTraceComplete: () => {
completeTrace?.();
completeTrace = null;
},
onTraceData: (data) => {
if (!traceOutputOpened || traceOutput?.writableEnded) {
return;
}
for (const event of data.value) {
traceOutput?.write(
`${traceFirstEvent ? '' : ','}${JSON.stringify(event)}`
);
traceFirstEvent = false;
}
},
traceComplete,
tracePath,
};
}
export async function startRendererDiagnosticCapture(
session: CDPSession,
artifacts: RendererArtifactCapture
): Promise<void> {
await session.send('Profiler.enable');
await session.send('Profiler.start');
artifacts.beginTraceOutput();
session.on('Tracing.dataCollected', artifacts.onTraceData);
session.on('Tracing.tracingComplete', artifacts.onTraceComplete);
await session.send('Tracing.start', {
categories: [
'blink.user_timing',
'devtools.timeline',
'disabled-by-default-devtools.timeline',
'disabled-by-default-v8.cpu_profiler',
'v8',
].join(','),
options: 'sampling-frequency=1000',
transferMode: 'ReportEvents',
});
}
export async function stopRendererDiagnosticCapture(
session: CDPSession,
artifacts: RendererArtifactCapture
): Promise<void> {
try {
const cpuProfile = await session.send('Profiler.stop');
await writeFile(
requirePath(artifacts.cpuProfilePath, 'renderer CPU profile'),
JSON.stringify(cpuProfile['profile']),
'utf8'
);
await session.send('Tracing.end');
await artifacts.traceComplete;
} finally {
disposeRendererDiagnosticListeners(session, artifacts);
await artifacts.finishTraceOutput();
}
}
export async function disposeRendererDiagnosticCapture(
session: CDPSession,
artifacts: RendererArtifactCapture
): Promise<void> {
disposeRendererDiagnosticListeners(session, artifacts);
await artifacts.finishTraceOutput();
}
export async function takeRendererHeapSnapshot(
session: CDPSession,
path: string | null
): Promise<void> {
const output = createWriteStream(
requirePath(path, 'renderer heap snapshot')
);
const onChunk = ({ chunk }: { readonly chunk: string }): void => {
output.write(chunk);
};
session.on('HeapProfiler.addHeapSnapshotChunk', onChunk);
try {
await session.send('HeapProfiler.takeHeapSnapshot', {
reportProgress: false,
});
} finally {
session.off('HeapProfiler.addHeapSnapshotChunk', onChunk);
output.end();
if (!output.writableFinished) {
await once(output, 'finish');
}
}
}
function disposeRendererDiagnosticListeners(
session: CDPSession,
artifacts: RendererArtifactCapture
): void {
session.off('Tracing.dataCollected', artifacts.onTraceData);
session.off('Tracing.tracingComplete', artifacts.onTraceComplete);
artifacts.onTraceComplete();
}
function requirePath(value: string | null, label: string): string {
if (value === null) {
throw new Error(`${label} path is unavailable`);
}
return value;
}
@@ -0,0 +1,313 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based lifecycle test. */
import { RENDERER_PERFORMANCE_PHASE_HOOK_KEY } from '@iptvnator/shared/logging';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { startM3uImportRendererCapture } from './m3u-import-renderer-capture';
import { M3U_IMPORT_RENDERER_STATE_KEY } from './m3u-import-renderer-probe';
test('idempotently disposes failed-iteration timers, probe, trace stream, listeners, and CDP session', async () => {
const browser = installFakeProbeEnvironment();
const outputDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-renderer-capture-dispose-')
);
const session = new FakeCdpSession();
const control = {
click: async () => undefined,
fill: async () => undefined,
first: () => control,
getByRole: () => control,
isDisabled: async () => false,
last: () => control,
waitFor: async () => undefined,
};
const page = {
...browser.page,
context: () => ({
newCDPSession: async () => session,
}),
getByRole: () => control,
locator: () => control,
};
try {
const capture = await startM3uImportRendererCapture(
page as never,
{
diagnostic: true,
outputDirectory,
playlistTitle: 'Synthetic',
playlistUrl:
'http://127.0.0.1:43210/synthetic-performance.m3u',
}
);
assert.equal(session.listenerCount('Tracing.dataCollected'), 1);
assert.equal(session.listenerCount('Tracing.tracingComplete'), 1);
await capture.dispose();
await capture.dispose();
assert.equal(session.detachCount, 1);
assert.equal(session.listenerCount('Tracing.dataCollected'), 0);
assert.equal(session.listenerCount('Tracing.tracingComplete'), 0);
assert.equal(browser.readState(), undefined);
const heapSamplesAfterDispose = session.heapUsageCalls;
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(session.heapUsageCalls, heapSamplesAfterDispose);
assert.deepEqual(
JSON.parse(
await readFile(
join(outputDirectory, 'renderer.trace.json'),
'utf8'
)
),
{ traceEvents: [] }
);
} finally {
browser.cleanup();
await rm(outputDirectory, { force: true, recursive: true });
}
});
test('failed-iteration disposal does not await a wedged renderer or CDP detach', async () => {
const browser = installFakeProbeEnvironment();
const outputDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-renderer-capture-wedged-')
);
const session = new FakeCdpSession({ hangDetach: true });
let evaluateCalls = 0;
const page = createFakePage(browser, session, async (callback, argument) => {
evaluateCalls += 1;
if (evaluateCalls > 1) {
return new Promise<never>(() => undefined);
}
return browser.page.evaluate(callback, argument);
});
try {
const capture = await startM3uImportRendererCapture(page as never, {
diagnostic: false,
outputDirectory,
playlistTitle: 'Synthetic',
playlistUrl:
'http://127.0.0.1:43210/synthetic-performance.m3u',
});
const outcome = await Promise.race([
capture.dispose().then(() => 'disposed'),
new Promise<'timeout'>((resolve) =>
setTimeout(() => resolve('timeout'), 150)
),
]);
assert.equal(outcome, 'disposed');
assert.equal(session.detachCount, 1);
const heapSamplesAfterDispose = session.heapUsageCalls;
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(session.heapUsageCalls, heapSamplesAfterDispose);
} finally {
browser.cleanup();
await rm(outputDirectory, { force: true, recursive: true });
}
});
test('rolls back partially started capture when Chromium tracing rejects', async () => {
const browser = installFakeProbeEnvironment();
const outputDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-renderer-capture-startup-')
);
const session = new FakeCdpSession({ failMethod: 'Tracing.start' });
const intervalToken = Object.freeze({});
const setIntervalDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'setInterval'
);
const clearIntervalDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'clearInterval'
);
let clearIntervalCalls = 0;
Object.defineProperty(globalThis, 'setInterval', {
configurable: true,
value: () => intervalToken,
writable: true,
});
Object.defineProperty(globalThis, 'clearInterval', {
configurable: true,
value: (token: unknown) => {
if (token === intervalToken) {
clearIntervalCalls += 1;
}
},
writable: true,
});
try {
await assert.rejects(
startM3uImportRendererCapture(
createFakePage(browser, session) as never,
{
diagnostic: true,
outputDirectory,
playlistTitle: 'Synthetic',
playlistUrl:
'http://127.0.0.1:43210/synthetic-performance.m3u',
}
),
/synthetic Tracing\.start failure/
);
assert.equal(clearIntervalCalls, 1);
assert.equal(session.detachCount, 1);
assert.equal(session.listenerCount('Tracing.dataCollected'), 0);
assert.equal(session.listenerCount('Tracing.tracingComplete'), 0);
assert.equal(browser.readState(), undefined);
assert.deepEqual(
JSON.parse(
await readFile(
join(outputDirectory, 'renderer.trace.json'),
'utf8'
)
),
{ traceEvents: [] }
);
} finally {
restoreGlobal('setInterval', setIntervalDescriptor);
restoreGlobal('clearInterval', clearIntervalDescriptor);
browser.cleanup();
await rm(outputDirectory, { force: true, recursive: true });
}
});
class FakeCdpSession extends EventEmitter {
detachCount = 0;
heapUsageCalls = 0;
constructor(
private readonly options: {
readonly failMethod?: string;
readonly hangDetach?: boolean;
} = {}
) {
super();
}
async detach(): Promise<void> {
this.detachCount += 1;
if (this.options.hangDetach) {
return new Promise<never>(() => undefined);
}
}
async send(method: string): Promise<Record<string, unknown>> {
if (method === this.options.failMethod) {
throw new Error(`synthetic ${method} failure`);
}
if (method === 'Runtime.getHeapUsage') {
this.heapUsageCalls += 1;
return { usedSize: 1_024 };
}
return {};
}
}
function installFakeProbeEnvironment() {
const descriptors = new Map<string, PropertyDescriptor | undefined>();
let nextFrameId = 1;
const replaceGlobal = (key: string, value: unknown): void => {
descriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
Object.defineProperty(globalThis, key, {
configurable: true,
value,
writable: true,
});
};
class FakePerformanceObserver {
static readonly supportedEntryTypes = ['longtask'];
disconnect(): undefined {
return undefined;
}
observe(): undefined {
return undefined;
}
takeRecords(): PerformanceEntry[] {
return [];
}
}
replaceGlobal('performance', { now: () => 0, timeOrigin: 0 });
replaceGlobal('PerformanceObserver', FakePerformanceObserver);
replaceGlobal('cancelAnimationFrame', () => undefined);
replaceGlobal('requestAnimationFrame', () => nextFrameId++);
replaceGlobal('location', { pathname: '/' });
replaceGlobal('document', { querySelector: () => null });
return {
cleanup: () => {
for (const [key, descriptor] of descriptors) {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
} else {
Reflect.deleteProperty(globalThis, key);
}
}
Reflect.deleteProperty(
globalThis,
Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY)
);
Reflect.deleteProperty(
globalThis,
M3U_IMPORT_RENDERER_STATE_KEY
);
},
page: {
evaluate: async (
callback: (argument: never) => unknown,
argument: never
) => callback(argument),
},
readState: () =>
(globalThis as Record<string, unknown>)[
M3U_IMPORT_RENDERER_STATE_KEY
],
};
}
function createFakePage(
browser: ReturnType<typeof installFakeProbeEnvironment>,
session: FakeCdpSession,
evaluate = browser.page.evaluate
) {
const control = {
click: async () => undefined,
fill: async () => undefined,
first: () => control,
getByRole: () => control,
isDisabled: async () => false,
last: () => control,
waitFor: async () => undefined,
};
return {
...browser.page,
context: () => ({
newCDPSession: async () => session,
}),
evaluate,
getByRole: () => control,
locator: () => control,
};
}
function restoreGlobal(
key: 'clearInterval' | 'setInterval',
descriptor: PropertyDescriptor | undefined
): void {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
} else {
Reflect.deleteProperty(globalThis, key);
}
}
@@ -0,0 +1,389 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import { RENDERER_PERFORMANCE_PHASE_HOOK_KEY } from '@iptvnator/shared/logging';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
interface ImportProbe {
firstChannelVisibleEpochMs: number | null;
frameGapsMs: readonly number[];
heartbeatDelaysMs: readonly number[];
longTasksMs: readonly number[];
operationStartEpochMs: number;
performancePhaseEvents: readonly unknown[];
playlistId: string | null;
routeReadyEpochMs: number | null;
terminalEpochMs: number | null;
uiPaintedEpochMs: number | null;
}
interface RendererCaptureModule {
advanceM3uImportHeartbeatDeadline?: (
deadlineEpochMs: number,
nowEpochMs: number
) => number;
assertCompleteM3uImportRendererProbe?: (probe: ImportProbe) => void;
startM3uImportRendererCapture?: unknown;
}
interface RendererProbeModule {
M3U_IMPORT_RENDERER_STATE_KEY?: string;
installM3uImportRendererProbe?: (page: unknown) => Promise<void>;
recordM3uImportHeartbeat?: (
page: unknown,
deadlineEpochMs: number
) => Promise<void>;
stopM3uImportRendererProbe?: (page: unknown) => Promise<ImportProbe>;
}
interface FakeProbeState {
frameGapsMs: number[];
heartbeatDelaysMs: number[];
operationStartEpochMs: number;
operationStartMonotonicMs: number;
terminalEpochMs: number | null;
}
const captureUrl = new URL('./m3u-import-renderer-capture.ts', import.meta.url);
const artifactsUrl = new URL(
'./m3u-import-renderer-artifacts.ts',
import.meta.url
);
const probeUrl = new URL('./m3u-import-renderer-probe.ts', import.meta.url);
const captureModulePromise = import(captureUrl.href)
.then((module) => module as RendererCaptureModule)
.catch(() => null);
const probeModulePromise = import(probeUrl.href)
.then((module) => module as unknown as RendererProbeModule)
.catch(() => null);
function installFakeBrowserEnvironment() {
const descriptors = new Map<string, PropertyDescriptor | undefined>();
const frames = new Map<number, (timestamp: number) => void>();
let nextFrameId = 1;
let nowMs = 0;
let observerRecords: PerformanceEntry[] | null = null;
const replaceGlobal = (key: string, value: unknown): void => {
descriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
Object.defineProperty(globalThis, key, {
configurable: true,
value,
writable: true,
});
};
class FakePerformanceObserver {
static readonly supportedEntryTypes = ['longtask'];
constructor() {
observerRecords = [];
}
disconnect(): undefined {
return undefined;
}
observe(): undefined {
return undefined;
}
takeRecords(): PerformanceEntry[] {
return observerRecords?.splice(0) ?? [];
}
}
replaceGlobal('performance', { now: () => nowMs, timeOrigin: 0 });
replaceGlobal('PerformanceObserver', FakePerformanceObserver);
replaceGlobal('cancelAnimationFrame', (requestId: number) => {
frames.delete(requestId);
});
replaceGlobal(
'requestAnimationFrame',
(callback: (timestamp: number) => void) => {
const requestId = nextFrameId++;
frames.set(requestId, callback);
return requestId;
}
);
replaceGlobal('location', { pathname: '/' });
replaceGlobal('document', { querySelector: () => null });
return {
cleanup: () => {
for (const [key, descriptor] of descriptors) {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
} else {
Reflect.deleteProperty(globalThis, key);
}
}
Reflect.deleteProperty(
globalThis,
Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY)
);
},
getObserver: () => observerRecords,
page: {
evaluate: async (
callback: (argument: unknown) => unknown,
argument: unknown
) => callback(argument),
},
readState: (stateKey: string) =>
(globalThis as Record<string, unknown>)[stateKey] as FakeProbeState,
runFrame: (timestamp: number) => {
nowMs = timestamp;
for (const [requestId, callback] of [...frames]) {
if (frames.delete(requestId)) {
callback(timestamp);
}
}
},
setNow: (value: number) => {
nowMs = value;
},
showTerminal: () => {
(
globalThis as unknown as {
location: { pathname: string };
}
).location.pathname =
'/Users/test/iptvnator/dist/apps/web/workspace/playlists/synthetic-id/all';
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: { querySelector: () => ({}) },
writable: true,
});
},
};
}
function readSource(url: URL): string | null {
try {
return readFileSync(url, 'utf8');
} catch {
return null;
}
}
function completeProbe(): ImportProbe {
return {
firstChannelVisibleEpochMs: 130,
frameGapsMs: [16, 48],
heartbeatDelaysMs: [0, 12],
longTasksMs: [72],
operationStartEpochMs: 100,
performancePhaseEvents: [],
playlistId: 'synthetic-id',
routeReadyEpochMs: 120,
terminalEpochMs: 150,
uiPaintedEpochMs: 150,
};
}
test('exports the formal M3U import renderer collector', async () => {
const module = await captureModulePromise;
assert.ok(module, 'M3U import renderer capture module must exist');
assert.equal(typeof module.startM3uImportRendererCapture, 'function');
assert.equal(
typeof module.assertCompleteM3uImportRendererProbe,
'function'
);
});
test('prepares the URL dialog before starting any measured capture', () => {
const source = readSource(captureUrl);
assert.ok(source, 'M3U import renderer capture source must exist');
const dialogPreparation = source.indexOf(
'await prepareM3uImportDialog(page, options)'
);
const cdpSession = source.indexOf('newCDPSession(page)');
const initialHeapSample = source.indexOf('await sampleHeap()');
assert.ok(dialogPreparation >= 0);
assert.ok(cdpSession > dialogPreparation);
assert.ok(initialHeapSample > cdpSession);
});
test('captures heap samples in every run and raw CPU, trace, and heap artifacts only in diagnostics', () => {
const source = readSource(captureUrl);
const artifactSource = readSource(artifactsUrl);
assert.ok(source);
assert.ok(artifactSource);
assert.match(source, /setInterval\(\(\) => void sampleHeap\(\), 20\)/);
assert.match(source, /Runtime\.getHeapUsage/);
assert.match(source, /HeapProfiler\.collectGarbage/);
assert.match(
artifactSource,
/cpuProfilePath: options\.diagnostic[\s\S]*renderer\.cpuprofile/
);
assert.match(artifactSource, /Profiler\.start/);
assert.match(artifactSource, /Tracing\.start/);
assert.match(artifactSource, /HeapProfiler\.takeHeapSnapshot/);
});
test('clips drained long tasks and heartbeat samples to the operation window', async () => {
const module = await probeModulePromise;
assert.ok(module);
assert.equal(typeof module.installM3uImportRendererProbe, 'function');
assert.equal(typeof module.recordM3uImportHeartbeat, 'function');
assert.equal(typeof module.stopM3uImportRendererProbe, 'function');
assert.equal(typeof module.M3U_IMPORT_RENDERER_STATE_KEY, 'string');
const browser = installFakeBrowserEnvironment();
try {
await module.installM3uImportRendererProbe?.(browser.page);
const state = browser.readState(
module.M3U_IMPORT_RENDERER_STATE_KEY ?? ''
);
state.operationStartEpochMs = 100;
state.operationStartMonotonicMs = 100;
state.terminalEpochMs = 200;
const observerRecords = browser.getObserver();
assert.ok(observerRecords);
observerRecords.push(
{ duration: 40, startTime: 80 } as PerformanceEntry,
{ duration: 30, startTime: 190 } as PerformanceEntry,
{ duration: 30, startTime: 210 } as PerformanceEntry
);
browser.setNow(250);
await module.recordM3uImportHeartbeat?.(browser.page, 90);
await module.recordM3uImportHeartbeat?.(browser.page, 150);
await module.recordM3uImportHeartbeat?.(browser.page, 210);
const metrics = await module.stopM3uImportRendererProbe?.(browser.page);
assert.deepEqual(metrics?.heartbeatDelaysMs, [50]);
assert.deepEqual(metrics?.longTasksMs, [20, 10]);
} finally {
browser.cleanup();
}
});
test('records the terminal animation-frame gap exactly once', async () => {
const module = await probeModulePromise;
assert.ok(module?.installM3uImportRendererProbe);
assert.equal(typeof module.M3U_IMPORT_RENDERER_STATE_KEY, 'string');
const browser = installFakeBrowserEnvironment();
try {
await module.installM3uImportRendererProbe(browser.page);
browser.runFrame(80);
const state = browser.readState(
module.M3U_IMPORT_RENDERER_STATE_KEY ?? ''
);
state.operationStartEpochMs = 100;
state.operationStartMonotonicMs = 100;
browser.showTerminal();
browser.runFrame(120);
browser.runFrame(140);
browser.runFrame(160);
assert.equal(state.terminalEpochMs, 160);
assert.deepEqual(state.frameGapsMs, [20, 20, 20]);
await module.stopM3uImportRendererProbe?.(browser.page);
} finally {
browser.cleanup();
}
});
test('advances the external heartbeat on its fixed 50ms deadline grid', async () => {
const module = await captureModulePromise;
assert.ok(module);
const advance = module.advanceM3uImportHeartbeatDeadline;
assert.equal(typeof advance, 'function');
assert.equal(advance?.(1_000, 1_005), 1_050);
assert.equal(advance?.(1_000, 1_060), 1_100);
assert.equal(advance?.(1_000, 1_200), 1_250);
});
test('installs the production renderer phase hook and returns its raw events', () => {
const source = readSource(probeUrl);
assert.ok(source);
assert.match(source, /RENDERER_PERFORMANCE_PHASE_HOOK_KEY/);
assert.match(source, /Symbol\.for\(hookKey\)/);
assert.match(source, /performancePhaseEvents\.push\(event\)/);
assert.match(
source,
/performancePhaseEvents: \[\.\.\.state\.performancePhaseEvents\]/
);
assert.match(source, /delete target\[hookSymbol\]/);
});
test('timestamps the click immediately before final Add and requires route, first item, and two animation frames', () => {
const source = readSource(probeUrl);
assert.ok(source);
const operationStart = source.indexOf(
'state.operationStartMonotonicMs = performance.now()'
);
const click = source.indexOf('button.click()', operationStart);
assert.ok(operationStart >= 0);
assert.ok(click > operationStart);
assert.ok(source.includes('/\\/workspace\\/playlists\\/([^/]+)\\/all$/'));
assert.match(source, /\[data-test-id="channel-item"\]/);
const firstPaintFrame = source.indexOf(
'state.uiPaintFrameRequestId = requestAnimationFrame'
);
const secondPaintFrame = source.indexOf(
'state.uiPaintFrameRequestId = requestAnimationFrame',
firstPaintFrame + 1
);
assert.ok(firstPaintFrame >= 0);
assert.ok(secondPaintFrame > firstPaintFrame);
for (const phase of [
'operationStartEpochMs',
'routeReadyEpochMs',
'firstChannelVisibleEpochMs',
'uiPaintedEpochMs',
'terminalEpochMs',
]) {
assert.match(source, new RegExp(phase));
}
});
test('fails closed when any terminal proof or the probe itself is missing', async () => {
const module = await captureModulePromise;
assert.ok(module);
const assertComplete = module.assertCompleteM3uImportRendererProbe;
assert.equal(typeof assertComplete, 'function');
assert.doesNotThrow(() => assertComplete?.(completeProbe()));
for (const field of [
'playlistId',
'routeReadyEpochMs',
'firstChannelVisibleEpochMs',
'uiPaintedEpochMs',
'terminalEpochMs',
] as const) {
const incomplete = { ...completeProbe(), [field]: null };
assert.throws(
() => assertComplete?.(incomplete),
/m3u-import-renderer-probe-incomplete/
);
}
assert.throws(
() =>
assertComplete?.({
...completeProbe(),
operationStartEpochMs: 0,
}),
/m3u-import-renderer-probe-incomplete/
);
});
test('clears the harness heap sampler even when the renderer probe is missing', () => {
const source = readSource(captureUrl);
assert.ok(source);
const backgroundStop = source.indexOf(
'await stopBackgroundWork()',
source.indexOf('const stopCapture')
);
const probeStop = source.indexOf('probe = await stopProbe()', backgroundStop);
assert.match(source, /clearInterval\(heapSampleTimer\)/);
assert.ok(backgroundStop >= 0);
assert.ok(probeStop > backgroundStop);
});
@@ -0,0 +1,366 @@
import type { CDPSession, Page } from '@playwright/test';
import type { NumericDistribution } from './m3u-refresh-cancellation-contract';
import {
createRendererArtifactCapture,
disposeRendererDiagnosticCapture,
type RendererArtifactCapture,
startRendererDiagnosticCapture,
stopRendererDiagnosticCapture,
takeRendererHeapSnapshot,
} from './m3u-import-renderer-artifacts';
import {
installM3uImportRendererProbe,
type M3uImportRendererProbeMetrics,
prepareM3uImportDialog,
recordM3uImportHeartbeat,
stopM3uImportRendererProbe,
triggerM3uImport,
waitForM3uImportTerminal,
} from './m3u-import-renderer-probe';
import { summarizeNumbers } from './performance-statistics';
const HEARTBEAT_INTERVAL_MS = 50;
export interface M3uImportRendererCaptureStartOptions {
readonly diagnostic: boolean;
readonly outputDirectory: string;
readonly playlistTitle: string;
readonly playlistUrl: string;
}
export interface M3uImportRendererPhaseTimestamps {
readonly firstChannelVisibleEpochMs: number;
readonly operationStartEpochMs: number;
readonly routeReadyEpochMs: number;
readonly terminalEpochMs: number;
readonly uiPaintedEpochMs: number;
}
export interface M3uImportRendererCaptureMetrics {
readonly cpuProfilePath: string | null;
readonly frameGap: NumericDistribution;
readonly heapSnapshotPath: string | null;
readonly heartbeatDelay: NumericDistribution;
readonly longTask: NumericDistribution;
readonly peakHeapUsedBytes: number;
readonly phaseTimestamps: M3uImportRendererPhaseTimestamps;
readonly postGcHeapUsedBytes: number;
readonly probe: M3uImportRendererProbeMetrics;
readonly tracePath: string | null;
}
export interface RunningM3uImportRendererCapture {
readonly session: CDPSession;
dispose(): Promise<void>;
stop(): Promise<M3uImportRendererCaptureMetrics>;
triggerImport(): Promise<number>;
waitForTerminal(): Promise<void>;
}
interface HeapUsageResult {
readonly usedSize: number;
}
export async function startM3uImportRendererCapture(
page: Page,
options: M3uImportRendererCaptureStartOptions
): Promise<RunningM3uImportRendererCapture> {
await prepareM3uImportDialog(page, options);
const session = await page.context().newCDPSession(page);
let probeInstalled = false;
let heapSampleTimer: NodeJS.Timeout | null = null;
let artifacts: RendererArtifactCapture | null = null;
const heapSamples: number[] = [];
let heapSampleError: unknown = null;
let heapSamplePromise: Promise<void> | null = null;
const sampleHeap = (): Promise<void> => {
heapSamplePromise ??= (async () => {
try {
const sample = (await session.send(
'Runtime.getHeapUsage'
)) as HeapUsageResult;
if (Number.isFinite(sample.usedSize)) {
heapSamples.push(sample.usedSize);
}
} catch (error) {
heapSampleError ??= error;
} finally {
heapSamplePromise = null;
}
})();
return heapSamplePromise;
};
try {
await session.send('Runtime.enable');
await session.send('Performance.enable');
await installM3uImportRendererProbe(page);
probeInstalled = true;
await sampleHeap();
throwCaptureError(heapSampleError, 'Renderer heap sampling failed');
heapSampleTimer = setInterval(() => void sampleHeap(), 20);
artifacts = createRendererArtifactCapture(options);
if (options.diagnostic) {
await startRendererDiagnosticCapture(session, artifacts);
}
} catch (error) {
if (heapSampleTimer) {
clearInterval(heapSampleTimer);
heapSampleTimer = null;
}
if (artifacts && options.diagnostic) {
await disposeRendererDiagnosticCapture(
session,
artifacts
).catch(() => undefined);
}
if (probeInstalled) {
continueBestEffort(() => stopM3uImportRendererProbe(page));
}
continueBestEffort(() => session.detach());
throw error;
}
if (!artifacts || !heapSampleTimer) {
throw new Error('M3U import renderer capture initialization failed');
}
let heartbeatDeadlineEpochMs: number | null = null;
let heartbeatError: unknown = null;
let heartbeatInFlight: Promise<void> | null = null;
let heartbeatTimer: NodeJS.Timeout | null = null;
let stopped = false;
let backgroundStopPromise: Promise<void> | null = null;
let probeStopPromise: Promise<M3uImportRendererProbeMetrics> | null = null;
let sessionDetachPromise: Promise<void> | null = null;
let abortPromise: Promise<void> | null = null;
let stopPromise: Promise<M3uImportRendererCaptureMetrics> | null = null;
let stopRequested = false;
let disposeRequested = false;
const stopSchedulingBackgroundWork = (): void => {
stopped = true;
if (heartbeatTimer) {
clearTimeout(heartbeatTimer);
heartbeatTimer = null;
}
if (heapSampleTimer) {
clearInterval(heapSampleTimer);
heapSampleTimer = null;
}
};
const scheduleHeartbeat = (): void => {
if (stopped || heartbeatDeadlineEpochMs === null) {
return;
}
const deadline = heartbeatDeadlineEpochMs;
heartbeatTimer = setTimeout(
() => {
heartbeatTimer = null;
heartbeatInFlight = recordM3uImportHeartbeat(page, deadline)
.catch((error: unknown) => {
heartbeatError ??= error;
})
.finally(() => {
heartbeatInFlight = null;
if (stopped) {
return;
}
heartbeatDeadlineEpochMs =
advanceM3uImportHeartbeatDeadline(
deadline,
Date.now()
);
scheduleHeartbeat();
});
},
Math.max(0, deadline - Date.now())
);
};
const stopBackgroundWork = (): Promise<void> => {
backgroundStopPromise ??= (async () => {
stopSchedulingBackgroundWork();
await heartbeatInFlight;
await heapSamplePromise;
await sampleHeap();
})();
return backgroundStopPromise;
};
const stopProbe = (): Promise<M3uImportRendererProbeMetrics> => {
probeStopPromise ??= stopM3uImportRendererProbe(page);
return probeStopPromise;
};
const detachSession = (): Promise<void> => {
sessionDetachPromise ??= session.detach();
return sessionDetachPromise;
};
const abortResources = (): Promise<void> => {
abortPromise ??= (async () => {
stopSchedulingBackgroundWork();
continueBestEffort(() => heartbeatInFlight ?? Promise.resolve());
continueBestEffort(() => heapSamplePromise ?? Promise.resolve());
continueBestEffort(stopProbe);
if (options.diagnostic) {
await disposeRendererDiagnosticCapture(
session,
artifacts
).catch(() => undefined);
}
continueBestEffort(detachSession);
})();
return abortPromise;
};
const stopCapture =
async (): Promise<M3uImportRendererCaptureMetrics> => {
try {
await stopBackgroundWork();
let probe: M3uImportRendererProbeMetrics | null = null;
let probeError: unknown = null;
try {
probe = await stopProbe();
} catch (error) {
probeError = error;
}
if (options.diagnostic) {
await stopRendererDiagnosticCapture(session, artifacts);
}
await session.send('HeapProfiler.enable');
await session.send('HeapProfiler.collectGarbage');
const postGc = (await session.send(
'Runtime.getHeapUsage'
)) as HeapUsageResult;
if (options.diagnostic) {
await takeRendererHeapSnapshot(
session,
artifacts.heapSnapshotPath
);
}
await detachSession();
throwCaptureError(
heapSampleError,
'Renderer heap sampling failed'
);
throwCaptureError(heartbeatError, 'Renderer heartbeat failed');
throwCaptureError(probeError, 'Renderer probe stop failed');
assertCompleteM3uImportRendererProbe(probe);
return Object.freeze({
cpuProfilePath: artifacts.cpuProfilePath,
frameGap: summarizeNumbers(probe.frameGapsMs),
heapSnapshotPath: artifacts.heapSnapshotPath,
heartbeatDelay: summarizeNumbers(
probe.heartbeatDelaysMs
),
longTask: summarizeNumbers(probe.longTasksMs),
peakHeapUsedBytes: Math.max(0, ...heapSamples),
phaseTimestamps: Object.freeze({
firstChannelVisibleEpochMs:
probe.firstChannelVisibleEpochMs,
operationStartEpochMs: probe.operationStartEpochMs,
routeReadyEpochMs: probe.routeReadyEpochMs,
terminalEpochMs: probe.terminalEpochMs,
uiPaintedEpochMs: probe.uiPaintedEpochMs,
}),
postGcHeapUsedBytes: postGc.usedSize,
probe,
tracePath: artifacts.tracePath,
});
} catch (error) {
await abortResources();
throw error;
}
};
return Object.freeze({
dispose: async () => {
disposeRequested = true;
if (stopPromise) {
continueBestEffort(() => stopPromise as Promise<unknown>);
}
await abortResources();
},
session,
stop: () => {
if (stopRequested || disposeRequested) {
return Promise.reject(
new Error('M3U import renderer capture already stopped')
);
}
stopRequested = true;
stopPromise = stopCapture();
return stopPromise;
},
triggerImport: async () => {
const operationStartEpochMs = await triggerM3uImport(page);
heartbeatDeadlineEpochMs =
operationStartEpochMs + HEARTBEAT_INTERVAL_MS;
scheduleHeartbeat();
return operationStartEpochMs;
},
waitForTerminal: () => waitForM3uImportTerminal(page),
});
}
export function advanceM3uImportHeartbeatDeadline(
deadlineEpochMs: number,
nowEpochMs: number
): number {
const nextDeadline = deadlineEpochMs + HEARTBEAT_INTERVAL_MS;
if (nextDeadline > nowEpochMs) {
return nextDeadline;
}
const missedIntervals =
Math.floor((nowEpochMs - nextDeadline) / HEARTBEAT_INTERVAL_MS) + 1;
return nextDeadline + missedIntervals * HEARTBEAT_INTERVAL_MS;
}
export function assertCompleteM3uImportRendererProbe(
probe: M3uImportRendererProbeMetrics | null | undefined
): asserts probe is M3uImportRendererProbeMetrics & {
readonly firstChannelVisibleEpochMs: number;
readonly playlistId: string;
readonly routeReadyEpochMs: number;
readonly terminalEpochMs: number;
readonly uiPaintedEpochMs: number;
} {
if (
!probe ||
typeof probe.playlistId !== 'string' ||
probe.playlistId.length === 0 ||
!isPositiveFinite(probe.operationStartEpochMs) ||
!isPositiveFinite(probe.routeReadyEpochMs) ||
!isPositiveFinite(probe.firstChannelVisibleEpochMs) ||
!isPositiveFinite(probe.uiPaintedEpochMs) ||
!isPositiveFinite(probe.terminalEpochMs)
) {
throw new Error('m3u-import-renderer-probe-incomplete');
}
if (
probe.routeReadyEpochMs < probe.operationStartEpochMs ||
probe.firstChannelVisibleEpochMs < probe.routeReadyEpochMs ||
probe.uiPaintedEpochMs < probe.firstChannelVisibleEpochMs ||
probe.terminalEpochMs < probe.uiPaintedEpochMs
) {
throw new Error('m3u-import-renderer-probe-incomplete');
}
}
function throwCaptureError(error: unknown, label: string): void {
if (error instanceof Error) {
throw error;
}
if (error !== null) {
throw new Error(`${label}: ${String(error)}`);
}
}
function continueBestEffort(work: () => Promise<unknown>): void {
try {
void work().catch(() => undefined);
} catch {
// The owning Electron app is closed immediately after abort cleanup.
}
}
function isPositiveFinite(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value > 0;
}
@@ -0,0 +1,378 @@
import type { Page } from '@playwright/test';
import {
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
type RendererPerformancePhaseEvent,
type RendererPerformancePhaseHook,
} from '@iptvnator/shared/logging';
export const M3U_IMPORT_RENDERER_STATE_KEY =
'__iptvnatorM3uImportRendererCapture';
export interface M3uImportDialogOptions {
readonly playlistTitle: string;
readonly playlistUrl: string;
}
export interface M3uImportRendererProbeMetrics {
readonly firstChannelVisibleEpochMs: number | null;
readonly frameGapsMs: readonly number[];
readonly heartbeatDelaysMs: readonly number[];
readonly longTasksMs: readonly number[];
readonly operationStartEpochMs: number;
readonly performancePhaseEvents: readonly RendererPerformancePhaseEvent[];
readonly playlistId: string | null;
readonly routeReadyEpochMs: number | null;
readonly terminalEpochMs: number | null;
readonly uiPaintedEpochMs: number | null;
}
export async function prepareM3uImportDialog(
page: Page,
options: M3uImportDialogOptions
): Promise<void> {
await page.getByRole('button', { name: 'Add playlist' }).first().click();
const dialog = page.locator('mat-dialog-container').last();
await dialog.waitFor({ state: 'visible' });
await dialog
.getByRole('textbox', { name: 'Playlist URL (m3u, m3u8)' })
.fill(options.playlistUrl);
await dialog
.getByRole('textbox', { name: 'Playlist title' })
.fill(options.playlistTitle);
const submit = dialog.getByRole('button', { name: 'Add playlist' }).last();
await submit.waitFor({ state: 'visible' });
if (await submit.isDisabled()) {
throw new Error('m3u-import-final-add-disabled');
}
}
export async function installM3uImportRendererProbe(page: Page): Promise<void> {
await page.evaluate(
({ hookKey, stateKey }) => {
interface ProbeState {
firstChannelVisibleEpochMs: number | null;
frameGapsMs: number[];
frameRequestId: number;
heartbeatDelaysMs: number[];
lastFrameAt: number | null;
longTaskObserver: PerformanceObserver | null;
longTasksMs: number[];
operationStartEpochMs: number;
operationStartMonotonicMs: number;
performancePhaseEvents: RendererPerformancePhaseEvent[];
playlistId: string | null;
recordFrameGap(timestamp: number): void;
recordLongTask(entry: PerformanceEntry): void;
routeReadyEpochMs: number | null;
terminalEpochMs: number | null;
uiPaintFrameRequestId: number | null;
uiPaintedEpochMs: number | null;
}
const target = globalThis as unknown as Record<
PropertyKey,
unknown
>;
const hookSymbol = Symbol.for(hookKey);
if (
target[stateKey] !== undefined ||
target[hookSymbol] !== undefined
) {
throw new Error('m3u-import-renderer-probe-already-installed');
}
const epoch = (): number =>
performance.timeOrigin + performance.now();
const state: ProbeState = {
firstChannelVisibleEpochMs: null,
frameGapsMs: [],
frameRequestId: 0,
heartbeatDelaysMs: [],
lastFrameAt: null,
longTaskObserver: null,
longTasksMs: [],
operationStartEpochMs: 0,
operationStartMonotonicMs: 0,
performancePhaseEvents: [],
playlistId: null,
recordFrameGap: (timestamp) => {
const previousFrameAt = state.lastFrameAt;
state.lastFrameAt = timestamp;
if (
previousFrameAt === null ||
state.operationStartEpochMs <= 0 ||
state.terminalEpochMs !== null
) {
return;
}
const measuredFrameStart = Math.max(
previousFrameAt,
state.operationStartMonotonicMs
);
if (timestamp > measuredFrameStart) {
state.frameGapsMs.push(timestamp - measuredFrameStart);
}
},
recordLongTask: (entry) => {
const entryEpochMs =
performance.timeOrigin + entry.startTime;
const measuredStartEpochMs = Math.max(
entryEpochMs,
state.operationStartEpochMs
);
const measuredEndEpochMs = Math.min(
entryEpochMs + entry.duration,
state.terminalEpochMs ?? Number.POSITIVE_INFINITY
);
if (
state.operationStartEpochMs > 0 &&
measuredEndEpochMs > measuredStartEpochMs
) {
state.longTasksMs.push(
measuredEndEpochMs - measuredStartEpochMs
);
}
},
routeReadyEpochMs: null,
terminalEpochMs: null,
uiPaintFrameRequestId: null,
uiPaintedEpochMs: null,
};
const performancePhaseHook: RendererPerformancePhaseHook = (
event
) => {
state.performancePhaseEvents.push(event);
};
target[hookSymbol] = performancePhaseHook;
const terminalTarget = (): string | null => {
const route = location.pathname.match(
/\/workspace\/playlists\/([^/]+)\/all$/
);
if (
!route ||
!document.querySelector('[data-test-id="channel-item"]')
) {
return null;
}
return route[1] ?? null;
};
const inspectTerminal = (): void => {
if (
state.operationStartEpochMs <= 0 ||
state.terminalEpochMs !== null
) {
return;
}
const route = location.pathname.match(
/\/workspace\/playlists\/([^/]+)\/all$/
);
if (!route) {
return;
}
state.routeReadyEpochMs ??= epoch();
if (!document.querySelector('[data-test-id="channel-item"]')) {
return;
}
state.firstChannelVisibleEpochMs ??= epoch();
if (state.uiPaintFrameRequestId !== null) {
return;
}
state.uiPaintFrameRequestId = requestAnimationFrame(() => {
state.uiPaintFrameRequestId = requestAnimationFrame(
(terminalFrameAt) => {
state.uiPaintFrameRequestId = null;
const playlistId = terminalTarget();
if (playlistId === null) {
return;
}
state.recordFrameGap(terminalFrameAt);
state.playlistId = playlistId;
state.uiPaintedEpochMs = epoch();
state.terminalEpochMs = state.uiPaintedEpochMs;
cancelAnimationFrame(state.frameRequestId);
state.frameRequestId = 0;
for (const entry of state.longTaskObserver?.takeRecords() ??
[]) {
state.recordLongTask(entry);
}
state.longTaskObserver?.disconnect();
state.longTaskObserver = null;
}
);
});
};
const frame = (timestamp: number): void => {
state.recordFrameGap(timestamp);
if (state.operationStartEpochMs > 0) {
inspectTerminal();
}
state.frameRequestId =
state.terminalEpochMs === null
? requestAnimationFrame(frame)
: 0;
};
state.frameRequestId = requestAnimationFrame(frame);
if (
typeof PerformanceObserver === 'function' &&
PerformanceObserver.supportedEntryTypes.includes('longtask')
) {
state.longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
state.recordLongTask(entry);
}
});
state.longTaskObserver.observe({ entryTypes: ['longtask'] });
}
target[stateKey] = state;
},
{
hookKey: RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
stateKey: M3U_IMPORT_RENDERER_STATE_KEY,
}
);
}
export async function triggerM3uImport(page: Page): Promise<number> {
return page.evaluate((stateKey) => {
const target = globalThis as unknown as Record<string, unknown>;
const state = target[stateKey] as
| {
operationStartEpochMs: number;
operationStartMonotonicMs: number;
}
| undefined;
if (!state || state.operationStartEpochMs > 0) {
throw new Error('m3u-import-renderer-probe-unavailable');
}
const dialogs = Array.from(
document.querySelectorAll('mat-dialog-container')
);
const dialog = dialogs.at(-1);
const button = Array.from(dialog?.querySelectorAll('button') ?? [])
.reverse()
.find(
(candidate) =>
candidate.textContent?.trim() === 'Add playlist' &&
!candidate.disabled
);
if (!(button instanceof HTMLButtonElement)) {
throw new Error('m3u-import-final-add-unavailable');
}
state.operationStartMonotonicMs = performance.now();
state.operationStartEpochMs =
performance.timeOrigin + state.operationStartMonotonicMs;
button.click();
return state.operationStartEpochMs;
}, M3U_IMPORT_RENDERER_STATE_KEY);
}
export async function recordM3uImportHeartbeat(
page: Page,
deadlineEpochMs: number
): Promise<void> {
await page.evaluate(
({ deadlineEpochMs, stateKey }) => {
const target = globalThis as unknown as Record<string, unknown>;
const state = target[stateKey] as
| {
heartbeatDelaysMs: number[];
operationStartEpochMs: number;
terminalEpochMs: number | null;
}
| undefined;
if (!state || state.operationStartEpochMs <= 0) {
throw new Error('m3u-import-renderer-probe-unavailable');
}
if (
deadlineEpochMs >= state.operationStartEpochMs &&
(state.terminalEpochMs === null ||
deadlineEpochMs <= state.terminalEpochMs)
) {
const receivedEpochMs =
performance.timeOrigin + performance.now();
const measuredEpochMs =
state.terminalEpochMs === null
? receivedEpochMs
: Math.min(receivedEpochMs, state.terminalEpochMs);
state.heartbeatDelaysMs.push(
Math.max(0, measuredEpochMs - deadlineEpochMs)
);
}
},
{ deadlineEpochMs, stateKey: M3U_IMPORT_RENDERER_STATE_KEY }
);
}
export async function waitForM3uImportTerminal(page: Page): Promise<void> {
await page
.waitForFunction(
(stateKey) => {
const target = globalThis as unknown as Record<string, unknown>;
const state = target[stateKey] as
{ terminalEpochMs: number | null } | undefined;
return typeof state?.terminalEpochMs === 'number';
},
M3U_IMPORT_RENDERER_STATE_KEY,
{ timeout: 120_000 }
)
.then(() => undefined);
}
export async function stopM3uImportRendererProbe(
page: Page
): Promise<M3uImportRendererProbeMetrics> {
return page.evaluate(
({ hookKey, stateKey }) => {
const target = globalThis as unknown as Record<
PropertyKey,
unknown
>;
const hookSymbol = Symbol.for(hookKey);
const state = target[stateKey] as
| {
firstChannelVisibleEpochMs: number | null;
frameGapsMs: number[];
frameRequestId: number;
heartbeatDelaysMs: number[];
longTaskObserver: PerformanceObserver | null;
longTasksMs: number[];
operationStartEpochMs: number;
performancePhaseEvents: RendererPerformancePhaseEvent[];
playlistId: string | null;
recordLongTask(entry: PerformanceEntry): void;
routeReadyEpochMs: number | null;
terminalEpochMs: number | null;
uiPaintFrameRequestId: number | null;
uiPaintedEpochMs: number | null;
}
| undefined;
if (!state) {
throw new Error('m3u-import-renderer-probe-unavailable');
}
cancelAnimationFrame(state.frameRequestId);
if (state.uiPaintFrameRequestId !== null) {
cancelAnimationFrame(state.uiPaintFrameRequestId);
}
for (const entry of state.longTaskObserver?.takeRecords() ?? []) {
state.recordLongTask(entry);
}
state.longTaskObserver?.disconnect();
delete target[stateKey];
delete target[hookSymbol];
return {
firstChannelVisibleEpochMs: state.firstChannelVisibleEpochMs,
frameGapsMs: [...state.frameGapsMs],
heartbeatDelaysMs: [...state.heartbeatDelaysMs],
longTasksMs: [...state.longTasksMs],
operationStartEpochMs: state.operationStartEpochMs,
performancePhaseEvents: [...state.performancePhaseEvents],
playlistId: state.playlistId,
routeReadyEpochMs: state.routeReadyEpochMs,
terminalEpochMs: state.terminalEpochMs,
uiPaintedEpochMs: state.uiPaintedEpochMs,
};
},
{
hookKey: RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
stateKey: M3U_IMPORT_RENDERER_STATE_KEY,
}
);
}
@@ -0,0 +1,338 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
import { deriveM3uImportPhaseMetrics } from './m3u-import-report';
import {
createTestMainCapture,
createTestRendererCapture,
TEST_M3U_CHANNEL_COUNT,
TEST_M3U_FIXTURE_BYTES,
} from './m3u-import-report.test-helpers';
describe('formal M3U import phase report', () => {
it('attributes every required phase without presenting indexes as zero work', () => {
const phases = deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: createTestMainCapture(),
renderer: createTestRendererCapture(),
});
assert.deepEqual(phases, {
angularRenderingMs: 10,
dataAcquireMs: 20,
databaseWorkerToMainCloneProxyMs: 12,
indexAndCommitMs: null,
indexAndCommitUnavailableReason:
'indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write',
ipcStructuredCloneProxyMs: 44,
mainToDatabaseWorkerCloneProxyMs: 2,
mainToRendererCloneProxyMs: 28,
m3uParsingMs: 40,
normalizationMs: 10,
playlistDeserializationMs: 12,
playlistSerializationMs: 20,
rendererToMainCloneProxyMs: 2,
sqliteReadMs: 5,
sqliteWriteMs: 30,
storeImportDispatchMs: 2,
storePublishChannelsMs: 10,
storeUpdateMs: 12,
totalMs: 215,
});
assert.equal(
phases.ipcStructuredCloneProxyMs,
phases.mainToRendererCloneProxyMs +
phases.rendererToMainCloneProxyMs +
phases.mainToDatabaseWorkerCloneProxyMs +
phases.databaseWorkerToMainCloneProxyMs
);
});
it('fails closed when any structured-clone boundary is missing, ambiguous, or out of order', () => {
const main = createTestMainCapture();
const databaseWorker = main.workers[0];
const upsert = databaseWorker?.requests[0];
assert.ok(databaseWorker);
assert.ok(upsert);
for (const invalidMain of [
{
...main,
timeline: main.timeline.filter(
(event) => event.type !== 'db-request'
),
},
{
...main,
timeline: [
...main.timeline,
main.timeline.find((event) => event.type === 'db-request'),
].filter(
(event): event is (typeof main.timeline)[number] =>
event !== undefined
),
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
sourceEpochMs: 1080,
},
...databaseWorker.requests.slice(1),
],
},
],
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
requestReceivedEpochMs: 1078,
},
...databaseWorker.requests.slice(1),
],
},
],
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
ipcCallId: null,
operationIdUnavailableReason:
'preload-performance-marker-missing',
sourceEpochMs: null,
},
...databaseWorker.requests.slice(1),
],
},
],
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
workStartedEpochMs: 1079,
},
...databaseWorker.requests.slice(1),
],
},
],
},
]) {
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: invalidMain,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
}
});
it('fails closed when a required phase, worker identity, or fixture count is ambiguous', () => {
const missingNormalize = createTestMainCapture();
const missingTimeline = missingNormalize.timeline.filter(
(event) => event.phase !== 'normalize'
);
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...missingNormalize,
timeline: missingTimeline,
},
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...createTestMainCapture(),
workers: [
...createTestMainCapture().workers,
{
...createTestMainCapture().workers[0],
ordinal: 2,
},
],
},
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
const wrongBytes = createTestMainCapture();
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES + 1,
main: wrongBytes,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
});
it('requires the route-reload GET and both worker-to-renderer return paths', () => {
const main = createTestMainCapture();
const databaseWorker = main.workers[0];
const upsert = databaseWorker?.requests[0];
const get = databaseWorker?.requests[1];
assert.ok(databaseWorker);
assert.ok(upsert);
assert.ok(get);
const invalidCaptures: MainCaptureMetrics[] = [
{
...main,
workers: [
{
...databaseWorker,
requests: databaseWorker.requests.slice(0, 1),
},
],
},
{
...main,
timeline: main.timeline.filter(
(event) =>
!(
event.type === 'preload-performance-success' &&
event.operation === 'DB_GET_APP_PLAYLIST'
)
),
},
{
...main,
timeline: main.timeline.filter(
(event) =>
!(
event.type === 'preload-performance-success' &&
event.operation === 'DB_UPSERT_APP_PLAYLIST'
)
),
},
{
...main,
timeline: main.timeline.map((event) =>
event.type === 'preload-performance-success' &&
event.operation === 'DB_GET_APP_PLAYLIST'
? {
...event,
sourceEpochMs: 1196,
}
: event
),
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
upsert,
{
...get,
responsePostedEpochMs: 1175,
},
],
},
],
},
];
for (const invalidMain of invalidCaptures) {
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: invalidMain,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
}
});
it('rejects playlist workers and cross-playlist database writes', () => {
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...createTestMainCapture(),
workers: [
...createTestMainCapture().workers,
{
kind: 'playlist-refresh.worker',
requests: [],
},
],
} as MainCaptureMetrics,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
const main = createTestMainCapture();
const databaseWorker = main.workers[0];
assert.ok(databaseWorker);
const upsert = databaseWorker.requests[0];
assert.ok(upsert);
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
playlistId: 'other-playlist',
},
],
},
],
},
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
});
});
@@ -0,0 +1,458 @@
/* eslint-disable max-lines -- The deterministic fixture keeps the full production-shaped import sequence visible. */
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
import type { M3uImportBenchmarkManifest } from './m3u-import-summary';
export const TEST_M3U_CHANNEL_COUNT = 10_000;
export const TEST_M3U_FIXTURE_BYTES = 1_024_000;
export function createTestMainCapture(): MainCaptureMetrics {
return {
timeline: [
mainPhase('start', 'data.acquire', 1001, null),
mainPhase('end', 'data.acquire', 1021, 20, {
byteCount: TEST_M3U_FIXTURE_BYTES,
}),
mainPhase('start', 'parse.m3u', 1021, null),
mainPhase('end', 'parse.m3u', 1061, 40, {
itemCount: TEST_M3U_CHANNEL_COUNT,
}),
mainPhase('start', 'normalize', 1061, null),
mainPhase('end', 'normalize', 1071, 10, {
itemCount: TEST_M3U_CHANNEL_COUNT,
}),
{
epochMs: 1080,
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-1',
sourceEpochMs: 1079,
type: 'db-request',
},
{
epochMs: 1137,
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-1',
success: true,
type: 'db-response',
},
{
epochMs: 1140,
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: 'playlist-1',
sourceEpochMs: 1140,
success: true,
type: 'preload-performance-success',
},
{
epochMs: 1142,
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-2',
sourceEpochMs: 1141,
type: 'db-request',
},
{
epochMs: 1174,
operation: 'DB_GET_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-2',
success: true,
type: 'db-response',
},
{
epochMs: 1194,
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
playlistId: 'playlist-1',
sourceEpochMs: 1194,
success: true,
type: 'preload-performance-success',
},
],
workers: [
{
kind: 'database.worker',
requests: [
{
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-uncorrelated',
performanceCaptureUnavailableReason: null,
phaseEvents: [
workerPhase(
'start',
'serialize.playlist',
1082,
null,
'db-request-1'
),
workerPhase(
'end',
'serialize.playlist',
1102,
20,
'db-request-1',
{
itemCount: TEST_M3U_CHANNEL_COUNT,
}
),
workerPhase(
'start',
'sqlite.write',
1102,
null,
'db-request-1'
),
workerPhase(
'end',
'sqlite.write',
1132,
30,
'db-request-1',
{
itemCount: 1,
}
),
],
playlistId: 'playlist-1',
requestId: 'db-request-1',
requestReceivedEpochMs: 1081,
responseEpochMs: 1137,
responsePostedEpochMs: 1135,
sourceEpochMs: 1079,
success: true,
workEndedEpochMs: 1133,
workStartedEpochMs: 1082,
},
{
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-uncorrelated',
performanceCaptureUnavailableReason: null,
phaseEvents: [
workerPhase(
'start',
'sqlite.read',
1144,
null,
'db-request-2'
),
workerPhase(
'end',
'sqlite.read',
1149,
5,
'db-request-2',
{
itemCount: 1,
}
),
workerPhase(
'start',
'deserialize.playlist',
1149,
null,
'db-request-2'
),
workerPhase(
'end',
'deserialize.playlist',
1161,
12,
'db-request-2',
{
itemCount: TEST_M3U_CHANNEL_COUNT,
}
),
],
playlistId: 'playlist-1',
requestId: 'db-request-2',
requestReceivedEpochMs: 1143,
responseEpochMs: 1174,
responsePostedEpochMs: 1164,
sourceEpochMs: 1141,
success: true,
workEndedEpochMs: 1162,
workStartedEpochMs: 1144,
},
],
},
],
} as unknown as MainCaptureMetrics;
}
export function createTestRendererCapture(): M3uImportRendererCaptureMetrics {
return {
phaseTimestamps: {
firstChannelVisibleEpochMs: 1210,
operationStartEpochMs: 1000,
routeReadyEpochMs: 1140,
terminalEpochMs: 1215,
uiPaintedEpochMs: 1215,
},
probe: {
performancePhaseEvents: [
rendererPhase(
'start',
'store.m3u-import-dispatch',
'renderer-1',
1076,
76
),
rendererPhase(
'end',
'store.m3u-import-dispatch',
'renderer-1',
1078,
78
),
rendererPhase(
'start',
'store.m3u-publish-channels',
'renderer-2',
1195,
195,
TEST_M3U_CHANNEL_COUNT
),
rendererPhase(
'end',
'store.m3u-publish-channels',
'renderer-2',
1205,
205,
TEST_M3U_CHANNEL_COUNT
),
],
playlistId: 'playlist-1',
},
} as unknown as M3uImportRendererCaptureMetrics;
}
export function createCompleteTestMainCapture(
overrides: { mainRss?: number; rendererRss?: number } = {}
): MainCaptureMetrics {
const base = createTestMainCapture();
const databaseWorker = base.workers[0];
const upsert = databaseWorker?.requests[0];
const get = databaseWorker?.requests[1];
if (!databaseWorker || !upsert || !get) {
throw new Error('invalid-test-main-capture');
}
return {
...base,
cpuProfilePath: null,
cpuSystemMicros: 20,
cpuUserMicros: 30,
eventLoopDelay: { maxMs: 3, p95Ms: 1, p99Ms: 2 },
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
'electron-main-embedded-event-loop',
heapSnapshotPath: null,
memory: {
peakHeapUsedBytes: 2_000,
peakRssBytes: overrides.mainRss ?? 5_000,
postGcHeapUsedBytes: 1_000,
postGcRssBytes: 4_500,
},
rendererWindow: {
responsiveEvents: 0,
rss: {
identity: { creationTime: 100, pid: 42 },
missingSampleCount: 0,
peakRssBytes: overrides.rendererRss ?? 4_000,
unavailableReason: null,
validSampleCount: 5,
},
unresponsiveEvents: 0,
windowIdentity: {
browserWindowId: 1,
webContentsId: 2,
},
},
rssScope:
'electron-main-process-including-worker-threads-and-native-memory',
workers: [
{
...databaseWorker,
externalMemorySampleCount: 4,
heapUsedSampleCount: 4,
peakExternalBytes: 600,
peakExternalUnavailableReason: null,
peakHeapUsedBytes: 1_500,
peakHeapUsedUnavailableReason: null,
postGcHeapUnavailableReason: null,
postGcHeapUsedBytes: 900,
requests: [
{
...upsert,
eventLoopDelay: { maxMs: 6, p95Ms: 4, p99Ms: 5 },
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: 0.8,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 1134,
invalidReason: null,
threadCpuSystemMicros: 40,
threadCpuUnavailableReason: null,
threadCpuUserMicros: 50,
},
{
...get,
eventLoopDelay: { maxMs: 9, p95Ms: 7, p99Ms: 8 },
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: 0.9,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 1163,
invalidReason: null,
threadCpuSystemMicros: 60,
threadCpuUnavailableReason: null,
threadCpuUserMicros: 70,
},
],
},
],
};
}
export function createCompleteTestRendererCapture(
overrides: { rendererHeap?: number } = {}
): M3uImportRendererCaptureMetrics {
const base = createTestRendererCapture();
return {
...base,
cpuProfilePath: null,
frameGap: distribution([16, 40]),
heapSnapshotPath: null,
heartbeatDelay: distribution([0, 5]),
longTask: distribution([60]),
peakHeapUsedBytes: overrides.rendererHeap ?? 3_000,
postGcHeapUsedBytes: 1_100,
probe: {
...base.probe,
firstChannelVisibleEpochMs: 1210,
frameGapsMs: [16, 40],
heartbeatDelaysMs: [0, 5],
longTasksMs: [60],
operationStartEpochMs: 1000,
routeReadyEpochMs: 1140,
terminalEpochMs: 1215,
uiPaintedEpochMs: 1215,
},
tracePath: null,
};
}
export function createTestM3uImportManifest(): M3uImportBenchmarkManifest {
return {
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
fixtureSha256: 'fixture-sha',
gitCommit: 'commit',
gitDiffSha256: 'diff',
gitDirty: false,
measuredRuns: 1,
memoryAccounting: {
databaseWorkerRss: 'unavailable-per-worker-thread',
mainRss:
'electron-main-process-including-worker-threads-and-native-memory',
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
rendererRss: 'separate-renderer-process',
workerJsHeap: 'separate-v8-isolate',
},
runtime: {
arch: 'arm64',
cdpPort: 9222,
electronVersion: '1',
harnessNodeVersion: '1',
platform: 'darwin',
},
scenario: 'm3u-import-10k',
syntheticSeed: 240_724,
variant: 'baseline',
warmupRuns: 1,
};
}
function mainPhase(
boundary: 'end' | 'start',
phase: string,
epochMs: number,
durationMs: number | null,
metadata: { byteCount?: number; itemCount?: number } = {}
) {
return {
boundary,
durationMs,
epochMs,
...metadata,
phase,
requestId: 'main-request-1',
type: 'm3u-import-phase',
};
}
function workerPhase(
boundary: 'end' | 'start',
phase:
| 'deserialize.playlist'
| 'serialize.playlist'
| 'sqlite.read'
| 'sqlite.write',
epochMs: number,
durationMs: number | null,
requestId: string,
metadata?: { byteCount?: number; itemCount?: number }
) {
return {
boundary,
durationMs,
epochMs,
...(metadata ? { metadata } : {}),
phase,
requestId,
};
}
function rendererPhase(
boundary: 'end' | 'start',
phase: 'store.m3u-import-dispatch' | 'store.m3u-publish-channels',
phaseId: string,
epochMs: number,
monotonicMs: number,
items?: number
) {
return {
boundary,
epochMs,
...(items === undefined ? {} : { metadata: { items } }),
monotonicMs,
...(boundary === 'end' ? { outcome: 'success' as const } : {}),
phase,
phaseId,
schemaVersion: 1 as const,
};
}
function distribution(values: readonly number[]) {
const min = Math.min(...values);
const max = Math.max(...values);
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
return {
count: values.length,
max,
mean,
median: mean,
min,
p95: values.length === 1 ? max : min + (max - min) * 0.95,
p99: values.length === 1 ? max : min + (max - min) * 0.99,
};
}
@@ -0,0 +1,367 @@
import {
APP_PLAYLIST_GET_PERFORMANCE_PHASE,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
M3U_IMPORT_PERFORMANCE_PHASE,
} from '@iptvnator/shared/interfaces';
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import type {
MainCaptureMetrics,
MainTimelineRecord,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import { deriveM3uImportIpcAttribution } from './m3u-import-ipc-attribution';
import {
pairPerformancePhaseEvents,
requirePerformancePhase,
type PairedPerformancePhase,
} from './performance-phase-events';
import {
pairRendererPerformancePhaseEvents,
requireRendererPerformancePhase,
} from './renderer-performance-phase-events';
export const M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON =
'indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write';
export interface M3uImportPhaseMetrics {
readonly angularRenderingMs: number;
readonly dataAcquireMs: number;
readonly databaseWorkerToMainCloneProxyMs: number;
readonly indexAndCommitMs: null;
readonly indexAndCommitUnavailableReason: typeof M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON;
readonly ipcStructuredCloneProxyMs: number;
readonly mainToDatabaseWorkerCloneProxyMs: number;
readonly mainToRendererCloneProxyMs: number;
readonly m3uParsingMs: number;
readonly normalizationMs: number;
readonly playlistDeserializationMs: number;
readonly playlistSerializationMs: number;
readonly rendererToMainCloneProxyMs: number;
readonly sqliteReadMs: number;
readonly sqliteWriteMs: number;
readonly storeImportDispatchMs: number;
readonly storePublishChannelsMs: number;
readonly storeUpdateMs: number;
readonly totalMs: number;
}
export interface M3uImportPhaseDerivationInput {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly main: MainCaptureMetrics;
readonly renderer: M3uImportRendererCaptureMetrics;
}
export function deriveM3uImportPhaseMetrics(
input: M3uImportPhaseDerivationInput
): M3uImportPhaseMetrics {
try {
return deriveM3uImportPhaseMetricsStrict(input);
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`m3u-import-phase-attribution-invalid:${detail}`);
}
}
function deriveM3uImportPhaseMetricsStrict(
input: M3uImportPhaseDerivationInput
): M3uImportPhaseMetrics {
requirePositiveSafeCount(input.channelCount, 'channel-count');
requirePositiveSafeCount(input.fixtureBytes, 'fixture-bytes');
const mainPhases = pairPerformancePhaseEvents(
input.main.timeline
.filter((event) => event.type === 'm3u-import-phase')
.map(toPerformancePhaseEvent)
);
if (mainPhases.length !== 3) {
throw new Error('main-phase-cardinality');
}
const mainRequestIds = new Set(mainPhases.map((phase) => phase.requestId));
if (mainRequestIds.size !== 1) {
throw new Error('main-request-correlation');
}
const mainRequestId = mainPhases[0]?.requestId;
if (!mainRequestId) {
throw new Error('main-request-missing');
}
const dataAcquire = requirePerformancePhase(
mainPhases,
mainRequestId,
M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE
);
const parseM3u = requirePerformancePhase(
mainPhases,
mainRequestId,
M3U_IMPORT_PERFORMANCE_PHASE.PARSE_M3U
);
const normalize = requirePerformancePhase(
mainPhases,
mainRequestId,
M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE
);
requireMetadataCount(
dataAcquire,
'byteCount',
input.fixtureBytes,
'data-acquire-bytes'
);
requireMetadataCount(
parseM3u,
'itemCount',
input.channelCount,
'parse-items'
);
requireMetadataCount(
normalize,
'itemCount',
input.channelCount,
'normalize-items'
);
requireOrdered(dataAcquire.endedEpochMs, parseM3u.startedEpochMs);
requireOrdered(parseM3u.endedEpochMs, normalize.startedEpochMs);
const rendererEvents = (
input.renderer.probe as typeof input.renderer.probe & {
readonly performancePhaseEvents?: readonly unknown[];
}
).performancePhaseEvents;
if (!rendererEvents) {
throw new Error('renderer-phase-events-missing');
}
const rendererPhases = pairRendererPerformancePhaseEvents(rendererEvents);
if (rendererPhases.length !== 2) {
throw new Error('renderer-phase-cardinality');
}
const importDispatch = requireRendererPerformancePhase(
rendererPhases,
'store.m3u-import-dispatch'
);
const publishChannels = requireRendererPerformancePhase(
rendererPhases,
'store.m3u-publish-channels'
);
requireRendererItems(
publishChannels.metadata?.items,
input.channelCount,
'publish-channels-items'
);
const playlistId = input.renderer.probe.playlistId;
if (typeof playlistId !== 'string' || playlistId.length === 0) {
throw new Error('renderer-playlist-id-missing');
}
const databaseWorkers = input.main.workers.filter(
(worker) => worker.kind === 'database.worker'
);
if (
databaseWorkers.length !== 1 ||
input.main.workers.some(
(worker) => worker.kind === 'playlist-refresh.worker'
)
) {
throw new Error('worker-cardinality');
}
const databaseWorker = databaseWorkers[0];
if (!databaseWorker) {
throw new Error('database-worker-missing');
}
const upserts = databaseWorker.requests.filter(
(request) =>
request.operation === 'DB_UPSERT_APP_PLAYLIST' &&
request.playlistId === playlistId &&
request.success
);
if (upserts.length !== 1 || !upserts[0]) {
throw new Error('database-upsert-correlation');
}
const upsert = upserts[0];
const gets = databaseWorker.requests.filter(
(request) =>
request.operation === 'DB_GET_APP_PLAYLIST' &&
request.playlistId === playlistId &&
request.success
);
if (
gets.length !== 1 ||
!gets[0] ||
databaseWorker.requests.length !== 2
) {
throw new Error('database-get-correlation');
}
const get = gets[0];
const databasePhases = pairPerformancePhaseEvents(upsert.phaseEvents);
if (
databasePhases.length !== 2 ||
!upsert.requestId ||
databasePhases.some((phase) => phase.requestId !== upsert.requestId)
) {
throw new Error('database-phase-cardinality');
}
const serializePlaylist = requireDatabasePhase(
databasePhases,
upsert,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST
);
const sqliteWrite = requireDatabasePhase(
databasePhases,
upsert,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE
);
requireMetadataCount(
serializePlaylist,
'itemCount',
input.channelCount,
'serialization-items'
);
requireMetadataCount(sqliteWrite, 'itemCount', 1, 'sqlite-row-count');
const getPhases = pairPerformancePhaseEvents(get.phaseEvents);
if (
getPhases.length !== 2 ||
!get.requestId ||
getPhases.some((phase) => phase.requestId !== get.requestId)
) {
throw new Error('database-get-phase-cardinality');
}
const sqliteRead = requireDatabasePhase(
getPhases,
get,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ
);
const deserializePlaylist = requireDatabasePhase(
getPhases,
get,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.DESERIALIZE_PLAYLIST
);
requireMetadataCount(sqliteRead, 'itemCount', 1, 'sqlite-read-row-count');
requireMetadataCount(
deserializePlaylist,
'itemCount',
input.channelCount,
'deserialization-items'
);
const operationStart = input.renderer.phaseTimestamps.operationStartEpochMs;
const uiPainted = input.renderer.phaseTimestamps.uiPaintedEpochMs;
const terminal = input.renderer.phaseTimestamps.terminalEpochMs;
requireOrdered(operationStart, dataAcquire.startedEpochMs);
requireOrdered(serializePlaylist.endedEpochMs, sqliteWrite.startedEpochMs);
requireOrdered(sqliteWrite.endedEpochMs, sqliteRead.startedEpochMs);
requireOrdered(sqliteRead.endedEpochMs, deserializePlaylist.startedEpochMs);
requireOrdered(
deserializePlaylist.endedEpochMs,
publishChannels.startedEpochMs
);
requireOrdered(publishChannels.endedEpochMs, uiPainted);
requireOrdered(uiPainted, terminal);
const ipc = deriveM3uImportIpcAttribution({
get,
getReadStartedEpochMs: sqliteRead.startedEpochMs,
importDispatchEndedEpochMs: importDispatch.endedEpochMs,
importDispatchStartedEpochMs: importDispatch.startedEpochMs,
normalizeEndedEpochMs: normalize.endedEpochMs,
playlistId,
publishChannelsStartedEpochMs: publishChannels.startedEpochMs,
serializeStartedEpochMs: serializePlaylist.startedEpochMs,
timeline: input.main.timeline,
upsert,
});
const angularRenderingMs = difference(
uiPainted,
publishChannels.endedEpochMs
);
const totalMs = difference(terminal, operationStart);
return Object.freeze({
angularRenderingMs,
dataAcquireMs: dataAcquire.durationMs,
indexAndCommitMs: null,
indexAndCommitUnavailableReason:
M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
...ipc,
m3uParsingMs: parseM3u.durationMs,
normalizationMs: normalize.durationMs,
playlistDeserializationMs: deserializePlaylist.durationMs,
playlistSerializationMs: serializePlaylist.durationMs,
sqliteReadMs: sqliteRead.durationMs,
sqliteWriteMs: sqliteWrite.durationMs,
storeImportDispatchMs: importDispatch.durationMs,
storePublishChannelsMs: publishChannels.durationMs,
storeUpdateMs: importDispatch.durationMs + publishChannels.durationMs,
totalMs,
});
}
function toPerformancePhaseEvent(event: MainTimelineRecord): unknown {
const metadata = {
...(event.byteCount === undefined
? {}
: { byteCount: event.byteCount }),
...(event.itemCount === undefined
? {}
: { itemCount: event.itemCount }),
};
return {
boundary: event.boundary,
durationMs: event.durationMs,
epochMs: event.epochMs,
...(Object.keys(metadata).length === 0 ? {} : { metadata }),
phase: event.phase,
requestId: event.requestId,
};
}
function requireDatabasePhase(
phases: readonly PairedPerformancePhase[],
request: WorkerRequestPerformanceMetrics,
phase: string
): PairedPerformancePhase {
if (!request.requestId) {
throw new Error('database-request-id-missing');
}
return requirePerformancePhase(phases, request.requestId, phase);
}
function requireMetadataCount(
phase: PairedPerformancePhase,
key: 'byteCount' | 'itemCount',
expected: number,
label: string
): void {
if (phase.metadata?.[key] !== expected) {
throw new Error(label);
}
}
function requireRendererItems(
value: number | undefined,
expected: number,
label: string
): void {
if (value !== expected) {
throw new Error(label);
}
}
function requirePositiveSafeCount(value: number, label: string): void {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(label);
}
}
function requireOrdered(startEpochMs: number, endEpochMs: number): void {
if (
!Number.isFinite(startEpochMs) ||
!Number.isFinite(endEpochMs) ||
endEpochMs < startEpochMs
) {
throw new Error('phase-order');
}
}
function difference(endEpochMs: number, startEpochMs: number): number {
requireOrdered(startEpochMs, endEpochMs);
return endEpochMs - startEpochMs;
}
@@ -0,0 +1,131 @@
import type { DatabaseWorkerPeakMemoryValidity } from './database-worker-peak-memory-validity';
import type { DatabaseWorkerRequestMetricsValidity } from './database-worker-request-metrics-validity';
import type { M3uImportScenarioId } from './m3u-import-benchmark-contract';
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import {
M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
type M3uImportPhaseMetrics,
} from './m3u-import-report';
import type {
DatabaseWorkerPostGcValidity,
MainCaptureMetrics,
NumericDistribution,
PerformanceIterationKind,
} from './m3u-refresh-cancellation-contract';
import type { RendererRssValidity } from './renderer-rss-validity';
export interface M3uImportIterationResult {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly kind: PerformanceIterationKind;
readonly main: MainCaptureMetrics;
readonly phases: M3uImportPhaseMetrics;
readonly renderer: M3uImportRendererCaptureMetrics;
readonly runId: string;
readonly scenarioId: M3uImportScenarioId;
}
export interface M3uImportBenchmarkManifest {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly fixtureSha256: string;
readonly gitCommit: string;
readonly gitDiffSha256: string;
readonly gitDirty: boolean;
readonly measuredRuns: number;
readonly memoryAccounting: {
readonly databaseWorkerRss: 'unavailable-per-worker-thread';
readonly mainRss: 'electron-main-process-including-worker-threads-and-native-memory';
readonly nativeAndSqliteMemory: 'included-in-main-rss-not-separately-attributable';
readonly rendererRss: 'separate-renderer-process';
readonly workerJsHeap: 'separate-v8-isolate';
};
readonly runtime: {
readonly arch: string;
readonly cdpPort: number;
readonly electronVersion: string;
readonly harnessNodeVersion: string;
readonly platform: string;
};
readonly scenario: M3uImportScenarioId;
readonly syntheticSeed: number;
readonly variant: string;
readonly warmupRuns: number;
}
export interface M3uImportMeasuredSummary {
readonly angularRenderingMs: NumericDistribution;
readonly dataAcquireMs: NumericDistribution;
readonly databaseWorkerGetEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerGetEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerGetEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerGetEventLoopUtilization: NumericDistribution;
readonly databaseWorkerGetThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerGetThreadCpuUserMicros: NumericDistribution;
readonly databaseWorkerEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerEventLoopUtilization: NumericDistribution;
readonly databaseWorkerExternalPeakBytes: NumericDistribution;
readonly databaseWorkerHeapPeakBytes: NumericDistribution;
readonly databaseWorkerPostGcHeapBytes: NumericDistribution;
readonly databaseWorkerThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerThreadCpuUserMicros: NumericDistribution;
readonly databaseWorkerToMainCloneProxyMs: NumericDistribution;
readonly databaseWorkerUpsertEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerUpsertEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerUpsertEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerUpsertEventLoopUtilization: NumericDistribution;
readonly databaseWorkerUpsertThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerUpsertThreadCpuUserMicros: NumericDistribution;
readonly ipcStructuredCloneProxyMs: NumericDistribution;
readonly mainToDatabaseWorkerCloneProxyMs: NumericDistribution;
readonly mainToRendererCloneProxyMs: NumericDistribution;
readonly m3uParsingMs: NumericDistribution;
readonly mainEventLoopDelayMaxMs: NumericDistribution;
readonly mainEventLoopDelayP95Ms: NumericDistribution;
readonly mainEventLoopDelayP99Ms: NumericDistribution;
readonly mainEventLoopUtilization: NumericDistribution;
readonly mainHeapPeakBytes: NumericDistribution;
readonly mainHeapPostGcBytes: NumericDistribution;
readonly mainRssPeakBytes: NumericDistribution;
readonly mainRssPostGcBytes: NumericDistribution;
readonly normalizationMs: NumericDistribution;
readonly playlistDeserializationMs: NumericDistribution;
readonly playlistSerializationMs: NumericDistribution;
readonly rendererFrameGapMs: NumericDistribution;
readonly rendererHeartbeatDelayMs: NumericDistribution;
readonly rendererHeapPeakBytes: NumericDistribution;
readonly rendererHeapPostGcBytes: NumericDistribution;
readonly rendererLongTaskCount: number;
readonly rendererLongTaskMs: NumericDistribution;
readonly rendererRssPeakBytes: NumericDistribution;
readonly rendererToMainCloneProxyMs: NumericDistribution;
readonly responsiveEvents: number;
readonly sqliteReadMs: NumericDistribution;
readonly sqliteWriteMs: NumericDistribution;
readonly storeImportDispatchMs: NumericDistribution;
readonly storePublishChannelsMs: NumericDistribution;
readonly storeUpdateMs: NumericDistribution;
readonly totalMs: NumericDistribution;
readonly unresponsiveEvents: number;
}
export interface M3uImportBenchmarkSummary {
readonly iterations: readonly M3uImportIterationResult[];
readonly manifest: M3uImportBenchmarkManifest;
readonly measured: M3uImportMeasuredSummary;
readonly notApplicable: {
readonly cancelAcknowledgementLatencyMs: 'scenario-has-no-cancellation';
readonly databaseWorkerRss: 'unavailable-per-worker-thread';
readonly indexAndCommitMs: typeof M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON;
readonly nativeAndSqliteMemory: 'included-in-main-rss-not-separately-attributable';
readonly playlistRefreshWorker: 'not-used-by-initial-import';
};
readonly validity: {
readonly databaseWorkerPeakMemory: DatabaseWorkerPeakMemoryValidity;
readonly databaseWorkerPostGc: DatabaseWorkerPostGcValidity;
readonly databaseWorkerRequestMetrics: DatabaseWorkerRequestMetricsValidity;
readonly rendererRss: RendererRssValidity;
};
}
@@ -0,0 +1,247 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { it } from 'node:test';
import { PERFORMANCE_ITERATION_KIND } from './m3u-refresh-cancellation-contract';
import {
createM3uImportBenchmarkSummary,
createM3uImportIterationResult,
type M3uImportBenchmarkSummary,
} from './m3u-import-summary';
import { assertM3uImportComparisonValidity } from './m3u-import.benchmark';
import {
createCompleteTestMainCapture,
createCompleteTestRendererCapture,
createTestM3uImportManifest,
TEST_M3U_CHANNEL_COUNT,
TEST_M3U_FIXTURE_BYTES,
} from './m3u-import-report.test-helpers';
it('keeps summary aggregation below the hard line limit without a suppression', () => {
const source = readFileSync(
resolve(process.cwd(), 'src/performance/m3u-import-summary.ts'),
'utf8'
);
assert.doesNotMatch(source, /eslint-disable max-lines/);
assert.ok(source.split(/\r?\n/).length <= 400);
});
it('summarizes measured runs only while preserving per-process memory boundaries', () => {
const warmup = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'warmup-01',
scenarioId: 'm3u-import-10k',
});
const measured = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'run-01',
scenarioId: 'm3u-import-10k',
});
const diagnostic = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
main: createCompleteTestMainCapture({
mainRss: 9_999,
rendererRss: 8_888,
}),
renderer: createCompleteTestRendererCapture({
rendererHeap: 7_777,
}),
runId: 'diagnostic',
scenarioId: 'm3u-import-10k',
});
const summary = createM3uImportBenchmarkSummary(
createTestM3uImportManifest(),
[warmup, measured, diagnostic]
);
assert.equal(summary.measured.totalMs.count, 1);
assert.equal(summary.measured.totalMs.mean, 215);
assert.equal(summary.measured.ipcStructuredCloneProxyMs.mean, 44);
assert.equal(summary.measured.mainToDatabaseWorkerCloneProxyMs.mean, 2);
assert.equal(summary.measured.databaseWorkerToMainCloneProxyMs.mean, 12);
assert.equal(summary.measured.mainToRendererCloneProxyMs.mean, 28);
assert.equal(summary.measured.rendererToMainCloneProxyMs.mean, 2);
assert.equal(summary.measured.sqliteReadMs.mean, 5);
assert.equal(summary.measured.playlistDeserializationMs.mean, 12);
assert.equal(summary.measured.databaseWorkerThreadCpuSystemMicros.mean, 100);
assert.equal(summary.measured.databaseWorkerThreadCpuUserMicros.mean, 120);
assert.equal(
summary.measured.databaseWorkerUpsertEventLoopDelayP95Ms.mean,
4
);
assert.equal(
summary.measured.databaseWorkerGetEventLoopDelayP95Ms.mean,
7
);
assert.equal(summary.measured.mainRssPeakBytes.mean, 5_000);
assert.equal(summary.measured.mainHeapPeakBytes.mean, 2_000);
assert.equal(summary.measured.rendererRssPeakBytes.mean, 4_000);
assert.equal(summary.measured.rendererHeapPeakBytes.mean, 3_000);
assert.equal(summary.measured.databaseWorkerHeapPeakBytes.mean, 1_500);
assert.equal(summary.measured.databaseWorkerPostGcHeapBytes.mean, 900);
assert.equal(summary.measured.rendererLongTaskCount, 1);
assert.equal(summary.measured.unresponsiveEvents, 0);
assert.deepEqual(summary.notApplicable, {
cancelAcknowledgementLatencyMs: 'scenario-has-no-cancellation',
databaseWorkerRss: 'unavailable-per-worker-thread',
indexAndCommitMs:
'indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write',
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
playlistRefreshWorker: 'not-used-by-initial-import',
});
assert.equal(summary.validity.rendererRss.validForComparison, true);
assert.equal(
summary.validity.databaseWorkerPeakMemory.validForComparison,
true
);
assert.equal(
summary.validity.databaseWorkerPostGc.validForComparison,
true
);
assert.doesNotThrow(() => assertM3uImportComparisonValidity(summary));
const databaseWorker = measured.main.workers[0];
assert.ok(databaseWorker);
const invalidMeasured = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: {
...measured.main,
workers: [
{
...databaseWorker,
externalMemorySampleCount: 0,
heapUsedSampleCount: 0,
peakExternalBytes: null,
peakExternalUnavailableReason:
'worker-external-memory-samples-missing',
peakHeapUsedBytes: null,
peakHeapUsedUnavailableReason:
'worker-heap-used-samples-missing',
},
],
},
renderer: measured.renderer,
runId: 'run-01',
scenarioId: 'm3u-import-10k',
});
const invalidPeakSummary = createM3uImportBenchmarkSummary(
createTestM3uImportManifest(),
[warmup, invalidMeasured, diagnostic]
);
assert.equal(
invalidPeakSummary.validity.databaseWorkerPeakMemory.validForComparison,
false
);
assert.equal(
invalidPeakSummary.measured.databaseWorkerHeapPeakBytes.count,
0
);
assert.throws(
() =>
assertM3uImportComparisonValidity(
invalidPeakSummary as M3uImportBenchmarkSummary
),
/Database worker peak-memory capture is invalid/
);
assert.throws(
() =>
createM3uImportBenchmarkSummary(createTestM3uImportManifest(), [
measured,
diagnostic,
]),
/m3u-import-summary-schedule-invalid/
);
});
it('fails comparison when any measured worker request omits mandatory event-loop or CPU metrics', () => {
const completeMain = createCompleteTestMainCapture();
const databaseWorker = completeMain.workers[0];
const get = databaseWorker?.requests[1];
assert.ok(databaseWorker);
assert.ok(get);
const invalidMain = {
...completeMain,
workers: [
{
...databaseWorker,
requests: [
databaseWorker.requests[0],
{
...get,
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
'event-loop-delay-capture-unavailable',
histogramFlushedEpochMs: null,
},
],
},
],
};
const warmup = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'warmup-01',
scenarioId: 'm3u-import-10k',
});
const measured = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: invalidMain,
renderer: createCompleteTestRendererCapture(),
runId: 'run-01',
scenarioId: 'm3u-import-10k',
});
const diagnostic = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'diagnostic',
scenarioId: 'm3u-import-10k',
});
const summary = createM3uImportBenchmarkSummary(
createTestM3uImportManifest(),
[warmup, measured, diagnostic]
);
assert.equal(
summary.validity.databaseWorkerRequestMetrics.validForComparison,
false
);
assert.equal(
summary.validity.databaseWorkerRequestMetrics.expectedRequestCount,
2
);
assert.equal(
summary.validity.databaseWorkerRequestMetrics.validRequestCount,
1
);
assert.equal(
summary.measured.databaseWorkerGetEventLoopDelayP95Ms.count,
0
);
assert.throws(
() => assertM3uImportComparisonValidity(summary),
/Database worker request metrics are invalid/
);
});
@@ -0,0 +1,382 @@
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import type { M3uImportScenarioId } from './m3u-import-benchmark-contract';
import type {
MainCaptureMetrics,
NumericDistribution,
PerformanceIterationKind,
WorkerCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
} from './m3u-refresh-cancellation-contract';
import {
deriveM3uImportPhaseMetrics,
M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
} from './m3u-import-report';
import { summarizeNumbers } from './performance-statistics';
import { assessDatabaseWorkerPeakMemoryValidity } from './database-worker-peak-memory-validity';
import { assessDatabaseWorkerPostGcValidity } from './database-worker-post-gc-validity';
import { assessDatabaseWorkerRequestMetricsValidity } from './database-worker-request-metrics-validity';
import { assessRendererRssValidity } from './renderer-rss-validity';
import type {
M3uImportBenchmarkManifest,
M3uImportBenchmarkSummary,
M3uImportIterationResult,
} from './m3u-import-summary-contract';
export type {
M3uImportBenchmarkManifest,
M3uImportBenchmarkSummary,
M3uImportIterationResult,
M3uImportMeasuredSummary,
} from './m3u-import-summary-contract';
export function createM3uImportIterationResult(input: {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly kind: PerformanceIterationKind;
readonly main: MainCaptureMetrics;
readonly renderer: M3uImportRendererCaptureMetrics;
readonly runId: string;
readonly scenarioId: M3uImportScenarioId;
}): M3uImportIterationResult {
return Object.freeze({
channelCount: input.channelCount,
fixtureBytes: input.fixtureBytes,
kind: input.kind,
main: input.main,
phases: deriveM3uImportPhaseMetrics(input),
renderer: input.renderer,
runId: input.runId,
scenarioId: input.scenarioId,
});
}
export function createM3uImportBenchmarkSummary(
manifest: M3uImportBenchmarkManifest,
iterations: readonly M3uImportIterationResult[]
): M3uImportBenchmarkSummary {
validateIterationSchedule(manifest, iterations);
const measured = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
);
validateMeasuredIterations(manifest, measured);
const values = (
select: (iteration: M3uImportIterationResult) => number | null
): NumericDistribution => summarizeNumbers(measured.map(select));
const rendererSamples = (
select: (
probe: M3uImportRendererCaptureMetrics['probe']
) => readonly number[]
): NumericDistribution =>
summarizeNumbers(
measured.flatMap((iteration) => [
...select(iteration.renderer.probe),
])
);
const databaseWorkers = measured.map(requireDatabaseWorker);
const databaseRequestPairs = databaseWorkers.map(
requireInitialImportRequests
);
const databaseUpserts = databaseRequestPairs.map((pair) => pair.upsert);
const databaseGets = databaseRequestPairs.map((pair) => pair.get);
const databaseRequests = databaseRequestPairs.flatMap((pair) => [
pair.upsert,
pair.get,
]);
return Object.freeze({
iterations: Object.freeze([...iterations]),
manifest,
measured: Object.freeze({
angularRenderingMs: values(
(iteration) => iteration.phases.angularRenderingMs
),
dataAcquireMs: values(
(iteration) => iteration.phases.dataAcquireMs
),
databaseWorkerGetEventLoopDelayMaxMs: summarizeNumbers(
databaseGets.map((request) => request.eventLoopDelay?.maxMs)
),
databaseWorkerGetEventLoopDelayP95Ms: summarizeNumbers(
databaseGets.map((request) => request.eventLoopDelay?.p95Ms)
),
databaseWorkerGetEventLoopDelayP99Ms: summarizeNumbers(
databaseGets.map((request) => request.eventLoopDelay?.p99Ms)
),
databaseWorkerGetEventLoopUtilization: summarizeNumbers(
databaseGets.map((request) => request.eventLoopUtilization)
),
databaseWorkerGetThreadCpuSystemMicros: summarizeNumbers(
databaseGets.map((request) => request.threadCpuSystemMicros)
),
databaseWorkerGetThreadCpuUserMicros: summarizeNumbers(
databaseGets.map((request) => request.threadCpuUserMicros)
),
databaseWorkerEventLoopDelayMaxMs: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopDelay?.maxMs)
),
databaseWorkerEventLoopDelayP95Ms: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopDelay?.p95Ms)
),
databaseWorkerEventLoopDelayP99Ms: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopDelay?.p99Ms)
),
databaseWorkerEventLoopUtilization: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopUtilization)
),
databaseWorkerExternalPeakBytes: summarizeNumbers(
databaseWorkers.map((worker) => worker.peakExternalBytes)
),
databaseWorkerHeapPeakBytes: summarizeNumbers(
databaseWorkers.map((worker) => worker.peakHeapUsedBytes)
),
databaseWorkerPostGcHeapBytes: summarizeNumbers(
databaseWorkers.map((worker) => worker.postGcHeapUsedBytes)
),
databaseWorkerThreadCpuSystemMicros: summarizeNumbers(
databaseRequestPairs.map((pair) =>
sumRequestMetric(
pair.upsert.threadCpuSystemMicros,
pair.get.threadCpuSystemMicros
)
)
),
databaseWorkerThreadCpuUserMicros: summarizeNumbers(
databaseRequestPairs.map((pair) =>
sumRequestMetric(
pair.upsert.threadCpuUserMicros,
pair.get.threadCpuUserMicros
)
)
),
databaseWorkerToMainCloneProxyMs: values(
(iteration) =>
iteration.phases.databaseWorkerToMainCloneProxyMs
),
databaseWorkerUpsertEventLoopDelayMaxMs: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopDelay?.maxMs)
),
databaseWorkerUpsertEventLoopDelayP95Ms: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopDelay?.p95Ms)
),
databaseWorkerUpsertEventLoopDelayP99Ms: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopDelay?.p99Ms)
),
databaseWorkerUpsertEventLoopUtilization: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopUtilization)
),
databaseWorkerUpsertThreadCpuSystemMicros: summarizeNumbers(
databaseUpserts.map(
(request) => request.threadCpuSystemMicros
)
),
databaseWorkerUpsertThreadCpuUserMicros: summarizeNumbers(
databaseUpserts.map((request) => request.threadCpuUserMicros)
),
ipcStructuredCloneProxyMs: values(
(iteration) => iteration.phases.ipcStructuredCloneProxyMs
),
mainToDatabaseWorkerCloneProxyMs: values(
(iteration) => iteration.phases.mainToDatabaseWorkerCloneProxyMs
),
mainToRendererCloneProxyMs: values(
(iteration) => iteration.phases.mainToRendererCloneProxyMs
),
m3uParsingMs: values((iteration) => iteration.phases.m3uParsingMs),
mainEventLoopDelayMaxMs: values(
(iteration) => iteration.main.eventLoopDelay.maxMs
),
mainEventLoopDelayP95Ms: values(
(iteration) => iteration.main.eventLoopDelay.p95Ms
),
mainEventLoopDelayP99Ms: values(
(iteration) => iteration.main.eventLoopDelay.p99Ms
),
mainEventLoopUtilization: values(
(iteration) => iteration.main.eventLoopUtilization
),
mainHeapPeakBytes: values(
(iteration) => iteration.main.memory.peakHeapUsedBytes
),
mainHeapPostGcBytes: values(
(iteration) => iteration.main.memory.postGcHeapUsedBytes
),
mainRssPeakBytes: values(
(iteration) => iteration.main.memory.peakRssBytes
),
mainRssPostGcBytes: values(
(iteration) => iteration.main.memory.postGcRssBytes
),
normalizationMs: values(
(iteration) => iteration.phases.normalizationMs
),
playlistDeserializationMs: values(
(iteration) => iteration.phases.playlistDeserializationMs
),
playlistSerializationMs: values(
(iteration) => iteration.phases.playlistSerializationMs
),
rendererFrameGapMs: rendererSamples((probe) => probe.frameGapsMs),
rendererHeartbeatDelayMs: rendererSamples(
(probe) => probe.heartbeatDelaysMs
),
rendererHeapPeakBytes: values(
(iteration) => iteration.renderer.peakHeapUsedBytes
),
rendererHeapPostGcBytes: values(
(iteration) => iteration.renderer.postGcHeapUsedBytes
),
rendererLongTaskCount: measured.reduce(
(total, iteration) =>
total + iteration.renderer.probe.longTasksMs.length,
0
),
rendererLongTaskMs: rendererSamples((probe) => probe.longTasksMs),
rendererRssPeakBytes: values(
(iteration) => iteration.main.rendererWindow.rss.peakRssBytes
),
rendererToMainCloneProxyMs: values(
(iteration) => iteration.phases.rendererToMainCloneProxyMs
),
responsiveEvents: measured.reduce(
(total, iteration) =>
total + iteration.main.rendererWindow.responsiveEvents,
0
),
sqliteReadMs: values((iteration) => iteration.phases.sqliteReadMs),
sqliteWriteMs: values(
(iteration) => iteration.phases.sqliteWriteMs
),
storeImportDispatchMs: values(
(iteration) => iteration.phases.storeImportDispatchMs
),
storePublishChannelsMs: values(
(iteration) => iteration.phases.storePublishChannelsMs
),
storeUpdateMs: values(
(iteration) => iteration.phases.storeUpdateMs
),
totalMs: values((iteration) => iteration.phases.totalMs),
unresponsiveEvents: measured.reduce(
(total, iteration) =>
total + iteration.main.rendererWindow.unresponsiveEvents,
0
),
}),
notApplicable: Object.freeze({
cancelAcknowledgementLatencyMs: 'scenario-has-no-cancellation',
databaseWorkerRss: 'unavailable-per-worker-thread',
indexAndCommitMs: M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
playlistRefreshWorker: 'not-used-by-initial-import',
}),
validity: Object.freeze({
databaseWorkerPeakMemory:
assessDatabaseWorkerPeakMemoryValidity(iterations),
databaseWorkerPostGc:
assessDatabaseWorkerPostGcValidity(iterations),
databaseWorkerRequestMetrics:
assessDatabaseWorkerRequestMetricsValidity(iterations),
rendererRss: assessRendererRssValidity(iterations),
}),
});
}
function validateIterationSchedule(
manifest: M3uImportBenchmarkManifest,
iterations: readonly M3uImportIterationResult[]
): void {
const warmupCount = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.WARMUP
).length;
const measuredCount = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
).length;
const diagnosticCount = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC
).length;
if (
warmupCount !== manifest.warmupRuns ||
measuredCount !== manifest.measuredRuns ||
diagnosticCount !== 1 ||
iterations.length !== warmupCount + measuredCount + diagnosticCount ||
new Set(iterations.map((iteration) => iteration.runId)).size !==
iterations.length ||
iterations.some(
(iteration) =>
iteration.channelCount !== manifest.channelCount ||
iteration.fixtureBytes !== manifest.fixtureBytes ||
iteration.scenarioId !== manifest.scenario
)
) {
throw new Error('m3u-import-summary-schedule-invalid');
}
}
function validateMeasuredIterations(
manifest: M3uImportBenchmarkManifest,
measured: readonly M3uImportIterationResult[]
): void {
if (
measured.length !== manifest.measuredRuns ||
new Set(measured.map((iteration) => iteration.runId)).size !==
measured.length ||
measured.some(
(iteration) =>
iteration.channelCount !== manifest.channelCount ||
iteration.fixtureBytes !== manifest.fixtureBytes ||
iteration.scenarioId !== manifest.scenario
)
) {
throw new Error('m3u-import-summary-measured-runs-invalid');
}
}
function requireDatabaseWorker(
iteration: M3uImportIterationResult
): WorkerCaptureMetrics {
const databaseWorkers = iteration.main.workers.filter(
(worker) => worker.kind === PERFORMANCE_WORKER_KIND.DATABASE
);
if (databaseWorkers.length !== 1 || !databaseWorkers[0]) {
throw new Error('m3u-import-summary-database-worker-invalid');
}
return databaseWorkers[0];
}
function requireInitialImportRequests(
worker: WorkerCaptureMetrics
): {
readonly get: WorkerRequestPerformanceMetrics;
readonly upsert: WorkerRequestPerformanceMetrics;
} {
const upserts = worker.requests.filter(
(request) =>
request.operation === 'DB_UPSERT_APP_PLAYLIST' && request.success
);
const gets = worker.requests.filter(
(request) =>
request.operation === 'DB_GET_APP_PLAYLIST' && request.success
);
if (
worker.requests.length !== 2 ||
upserts.length !== 1 ||
!upserts[0] ||
gets.length !== 1 ||
!gets[0]
) {
throw new Error('m3u-import-summary-database-request-invalid');
}
return { get: gets[0], upsert: upserts[0] };
}
function sumRequestMetric(
first: number | null,
second: number | null
): number | null {
return first === null || second === null ? null : first + second;
}
@@ -0,0 +1,341 @@
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
closeElectronApp,
expect,
launchElectronApp,
type LaunchedElectronApp,
} from '../electron-test-fixtures';
import { validateInitialImportDiagnosticArtifacts } from './initial-import-diagnostic-artifacts';
import {
assertRendererCdpTarget,
assertTcpPortAvailable,
type M3uImportBenchmarkConfiguration,
resolveM3uImportBenchmarkConfiguration,
resolveRendererWindowIdentity,
writeM3uImportJson as writeJson,
} from './m3u-import-benchmark-support';
import {
assertSyntheticM3uImportUrl,
createM3uImportIterationDefinitions,
listM3uImportScenarios,
type M3uImportIterationDefinition,
type M3uImportScenario,
} from './m3u-import-benchmark-contract';
import {
createM3uImportBenchmarkSummary,
createM3uImportIterationResult,
type M3uImportBenchmarkManifest,
type M3uImportBenchmarkSummary,
type M3uImportIterationResult,
} from './m3u-import-summary';
import {
startM3uImportRendererCapture,
type RunningM3uImportRendererCapture,
} from './m3u-import-renderer-capture';
import {
installMainCapture,
readMainCaptureStatus,
startMainCapture,
stopMainCapture,
} from './m3u-refresh-main-capture';
import { PERFORMANCE_ITERATION_KIND } from './m3u-refresh-cancellation-contract';
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
import {
startSyntheticM3uServer,
type SyntheticM3uServer,
} from './synthetic-m3u-server';
import {
createSyntheticM3uFixture,
SYNTHETIC_M3U_SEED,
type SyntheticM3uFixture,
} from './synthetic-m3u';
export async function runM3uImportBenchmark(): Promise<void> {
const config = await resolveM3uImportBenchmarkConfiguration();
const definitions = createM3uImportIterationDefinitions(config.smoke);
for (const scenario of listM3uImportScenarios()) {
await runScenario(
config,
scenario,
definitions.filter(
(definition) => definition.scenarioId === scenario.id
)
);
}
}
async function runScenario(
config: M3uImportBenchmarkConfiguration,
scenario: M3uImportScenario,
definitions: readonly M3uImportIterationDefinition[]
): Promise<void> {
const fixture = createSyntheticM3uFixture(scenario.channelCount);
const server = await startSyntheticM3uServer(fixture);
const scenarioDirectory = join(config.variantDirectory, scenario.id);
const iterations: M3uImportIterationResult[] = [];
try {
assertSyntheticM3uImportUrl(server.resourceUrl);
await server.prime();
await assertPerformanceArtifactCapacity(scenarioDirectory);
await mkdir(scenarioDirectory, { recursive: false });
for (const definition of definitions) {
console.log(
`[performance] ${config.variant}/${scenario.id}/${definition.runId} starting`
);
const result = await runIteration(
config,
definition,
fixture,
server,
scenarioDirectory
);
iterations.push(result);
console.log(
`[performance] ${definition.runId} total=${result.phases.totalMs.toFixed(3)}ms`
);
}
} finally {
await server.close();
}
if (server.requestCount() !== definitions.length + 1) {
throw new Error('Synthetic M3U server request total is invalid');
}
const manifest = createManifest(config, scenario, fixture);
const summary = createM3uImportBenchmarkSummary(manifest, iterations);
await writeJson(join(scenarioDirectory, 'manifest.json'), manifest);
await writeJson(join(scenarioDirectory, 'summary.json'), summary);
const diagnostic = requireDiagnosticIteration(iterations);
await validateInitialImportDiagnosticArtifacts(
{ main: diagnostic.main, renderer: diagnostic.renderer },
join(scenarioDirectory, diagnostic.runId)
);
assertM3uImportComparisonValidity(summary);
console.log(
`[performance] completed: ${join(scenarioDirectory, 'summary.json')}`
);
}
export function assertM3uImportComparisonValidity(
summary: M3uImportBenchmarkSummary
): void {
if (!summary.validity.rendererRss.validForComparison) {
throw new Error(
`Renderer RSS capture is invalid for comparison: ${JSON.stringify(
summary.validity.rendererRss.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerPostGc.validForComparison) {
throw new Error(
`Database worker post-GC capture is invalid for comparison: ${JSON.stringify(
summary.validity.databaseWorkerPostGc.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerPeakMemory.validForComparison) {
throw new Error(
`Database worker peak-memory capture is invalid for comparison: ${JSON.stringify(
summary.validity.databaseWorkerPeakMemory.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerRequestMetrics.validForComparison) {
throw new Error(
`Database worker request metrics are invalid for comparison: ${JSON.stringify(
summary.validity.databaseWorkerRequestMetrics.invalidRequests
)}`
);
}
}
async function runIteration(
config: M3uImportBenchmarkConfiguration,
definition: M3uImportIterationDefinition,
fixture: SyntheticM3uFixture,
server: SyntheticM3uServer,
scenarioDirectory: string
): Promise<M3uImportIterationResult> {
const iterationDirectory = join(scenarioDirectory, definition.runId);
await assertPerformanceArtifactCapacity(iterationDirectory);
await mkdir(iterationDirectory, { recursive: false });
const dataDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-m3u-import-performance-')
);
let app: LaunchedElectronApp | null = null;
let renderer: RunningM3uImportRendererCapture | null = null;
try {
await assertTcpPortAvailable(config.rendererCdpPort);
app = await launchElectronApp(dataDirectory, {
args: [
'--js-flags=--expose-gc',
'--remote-debugging-address=127.0.0.1',
`--remote-debugging-port=${config.rendererCdpPort}`,
`--user-data-dir=${join(dataDirectory, 'user-data')}`,
],
env: {
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
IPTVNATOR_PERF_CAPTURE: '1',
IPTVNATOR_PERF_WORKER_PROFILING: '1',
IPTVNATOR_TRACE_RENDERER_CONSOLE:
process.env['IPTVNATOR_TRACE_RENDERER_CONSOLE'] ?? '0',
},
});
app.mainWindow.setDefaultTimeout(120_000);
await assertRendererCdpTarget(
app.mainWindow,
config.rendererCdpPort
);
await installMainCapture(app.electronApp);
const rendererWindowIdentity = await resolveRendererWindowIdentity(app);
const requestsBefore = server.requestCount();
const diagnostic =
definition.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC;
renderer = await startM3uImportRendererCapture(app.mainWindow, {
diagnostic,
outputDirectory: iterationDirectory,
playlistTitle: `Synthetic M3U import ${definition.channelCount}`,
playlistUrl: server.resourceUrl,
});
await startMainCapture(app.electronApp, {
diagnostic,
outputDirectory: iterationDirectory,
rendererWindowIdentity,
});
await renderer.triggerImport();
await renderer.waitForTerminal();
await waitForDatabaseSettlement(app);
assertM3uImportRequestDelta(requestsBefore, server.requestCount());
const [rendererMetrics, mainMetrics] = await Promise.all([
renderer.stop(),
stopMainCapture(app.electronApp),
]);
await Promise.all([
writeJson(
join(iterationDirectory, 'main-capture.json'),
mainMetrics
),
writeJson(
join(iterationDirectory, 'renderer-capture.json'),
rendererMetrics
),
]);
const result = createM3uImportIterationResult({
channelCount: definition.channelCount,
fixtureBytes: fixture.bytes,
kind: definition.kind,
main: mainMetrics,
renderer: rendererMetrics,
runId: definition.runId,
scenarioId: definition.scenarioId,
});
await writeJson(join(iterationDirectory, 'result.json'), result);
return result;
} finally {
if (renderer) {
await renderer.dispose().catch((error: unknown) => {
console.error(
'[performance] Renderer capture cleanup failed',
error
);
});
}
if (app) {
await closeElectronApp(app).catch((error: unknown) => {
console.error('[performance] Electron cleanup failed', error);
});
}
await rm(dataDirectory, { force: true, recursive: true });
}
}
async function waitForDatabaseSettlement(
app: LaunchedElectronApp
): Promise<void> {
await expect
.poll(
async () => {
const status = await readMainCaptureStatus(app.electronApp);
return (
status.databaseRequests === 2 &&
status.databaseUpsertsCompleted === 1 &&
status.databaseGetsCompleted === 1 &&
status.databasePending === 0 &&
status.preloadSuccessMarkers === 2
);
},
{ timeout: 120_000 }
)
.toBe(true);
}
function createManifest(
config: M3uImportBenchmarkConfiguration,
scenario: M3uImportScenario,
fixture: SyntheticM3uFixture
): M3uImportBenchmarkManifest {
return Object.freeze({
channelCount: fixture.channelCount,
fixtureBytes: fixture.bytes,
fixtureSha256: fixture.sha256,
gitCommit: config.sourceState.commit,
gitDiffSha256: config.sourceState.diffSha256,
gitDirty: config.sourceState.dirty,
measuredRuns: config.measuredRuns,
memoryAccounting: Object.freeze({
databaseWorkerRss: 'unavailable-per-worker-thread',
mainRss:
'electron-main-process-including-worker-threads-and-native-memory',
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
rendererRss: 'separate-renderer-process',
workerJsHeap: 'separate-v8-isolate',
}),
runtime: Object.freeze({
arch: process.arch,
cdpPort: config.rendererCdpPort,
electronVersion: config.electronVersion,
harnessNodeVersion: process.versions.node,
platform: process.platform,
}),
scenario: scenario.id,
syntheticSeed: SYNTHETIC_M3U_SEED,
variant: config.variant,
warmupRuns: 1,
});
}
function requireDiagnosticIteration(
iterations: readonly M3uImportIterationResult[]
): M3uImportIterationResult {
const diagnostic = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC
);
if (diagnostic.length !== 1 || !diagnostic[0]) {
throw new Error('M3U import diagnostic iteration is missing');
}
return diagnostic[0];
}
export function assertM3uImportRequestDelta(
before: number,
after: number
): void {
const oneRequest =
Number.isSafeInteger(before) &&
Number.isSafeInteger(after) &&
after === before + 1;
if (!oneRequest) {
throw new Error(
'Synthetic M3U import must issue exactly one application GET'
);
}
}
@@ -1,7 +1,9 @@
import type { RendererProcessRssCapture } from './renderer-process-rss-capture';
import type { RendererRssValidity } from './renderer-rss-validity';
import type {
RendererProcessRssCapture,
RendererProcessRssUnavailableReason,
} from './renderer-process-rss-capture';
AppPlaylistPerformancePhase,
PerformancePhaseEvent,
} from '@iptvnator/shared/interfaces';
export const PERFORMANCE_ITERATION_KIND = {
DIAGNOSTIC: 'diagnostic',
@@ -54,6 +56,12 @@ export type WorkerPostGcHeapCapture =
readonly postGcHeapUsedBytes: null;
};
export const WORKER_PEAK_MEMORY_UNAVAILABLE_REASON = {
EXTERNAL_MEMORY_SAMPLES_MISSING:
'worker-external-memory-samples-missing',
HEAP_USED_SAMPLES_MISSING: 'worker-heap-used-samples-missing',
} as const;
export interface NumericDistribution {
readonly count: number;
readonly max: number | null;
@@ -78,12 +86,19 @@ export interface ProcessMemoryMetrics {
}
export interface MainTimelineRecord {
readonly boundary?: 'end' | 'start';
readonly byteCount?: number;
readonly durationMs?: number | null;
readonly epochMs: number;
readonly ipcCallId?: number;
readonly itemCount?: number;
readonly operation?: string;
readonly operationId?: string;
readonly phase?: string;
readonly playlistId?: string;
readonly requestId?: string;
readonly success?: boolean;
readonly sourceEpochMs?: number;
readonly type: string;
}
@@ -93,15 +108,19 @@ export interface WorkerRequestPerformanceMetrics {
readonly eventLoopUtilization: number | null;
readonly eventLoopUtilizationUnavailableReason: string | null;
readonly histogramFlushedEpochMs: number | null;
readonly ipcCallId: number | null;
readonly invalidReason: string | null;
readonly operation: string | null;
readonly operationId: string | null;
readonly operationIdUnavailableReason: string | null;
readonly performanceCaptureUnavailableReason: string | null;
readonly phaseEvents: readonly PerformancePhaseEvent<AppPlaylistPerformancePhase>[];
readonly playlistId: string | null;
readonly requestId: string | null;
readonly requestReceivedEpochMs: number | null;
readonly responseEpochMs: number;
readonly responsePostedEpochMs: number | null;
readonly sourceEpochMs: number | null;
readonly success: boolean;
readonly threadCpuSystemMicros: number | null;
readonly threadCpuUnavailableReason: string | null;
@@ -117,11 +136,19 @@ export type WorkerCaptureMetrics = {
readonly eventLoopDelay: EventLoopDelayMetrics | null;
readonly eventLoopDelayUnavailableReason: string | null;
readonly eventLoopUtilization: number | null;
readonly externalMemorySampleCount: number;
readonly heapUsedSampleCount: number;
readonly kind: PerformanceWorkerKind;
readonly operationId: string | null;
readonly ordinal: number;
readonly peakExternalBytes: number;
readonly peakHeapUsedBytes: number;
readonly peakExternalBytes: number | null;
readonly peakExternalUnavailableReason:
| typeof WORKER_PEAK_MEMORY_UNAVAILABLE_REASON.EXTERNAL_MEMORY_SAMPLES_MISSING
| null;
readonly peakHeapUsedBytes: number | null;
readonly peakHeapUsedUnavailableReason:
| typeof WORKER_PEAK_MEMORY_UNAVAILABLE_REASON.HEAP_USED_SAMPLES_MISSING
| null;
readonly playlistId: string | null;
readonly profilePath: string | null;
readonly requests: readonly WorkerRequestPerformanceMetrics[];
@@ -257,22 +284,6 @@ export interface DatabaseWorkerPostGcValidity {
readonly validMeasuredRunCount: number;
}
export interface InvalidRendererRssMeasuredRun {
readonly missingSampleCount: number;
readonly reason:
RendererProcessRssUnavailableReason | 'renderer-rss-capture-invalid';
readonly runId: string;
readonly validSampleCount: number;
}
export interface RendererRssValidity {
readonly invalidMeasuredRuns: readonly InvalidRendererRssMeasuredRun[];
readonly measuredRunCount: number;
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
export interface CancellationBenchmarkSummary {
readonly cancellationEffectRate: number;
readonly iterations: readonly CancellationIterationResult[];
@@ -9,7 +9,6 @@ import type {
NumericDistribution,
PerformanceWorkerKind,
RendererCaptureMetrics,
RendererRssValidity,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
@@ -21,6 +20,7 @@ import {
summarizeNumbers,
} from './performance-statistics';
import { assessDatabaseWorkerPostGcValidity } from './database-worker-post-gc-validity';
import { assessRendererRssValidity } from './renderer-rss-validity';
export function createCancellationIterationResult(input: {
readonly kind: CancellationIterationResult['kind'];
@@ -316,50 +316,6 @@ export function createCancellationBenchmarkSummary(
});
}
function assessRendererRssValidity(
iterations: readonly CancellationIterationResult[]
): RendererRssValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
);
const invalidMeasuredRuns: RendererRssValidity['invalidMeasuredRuns'][number][] =
[];
let validMeasuredRunCount = 0;
for (const iteration of measured) {
const rss = iteration.main.rendererWindow.rss;
if (
rss.identity !== null &&
Number.isSafeInteger(rss.peakRssBytes) &&
Number(rss.peakRssBytes) > 0 &&
rss.unavailableReason === null &&
rss.missingSampleCount === 0 &&
Number.isSafeInteger(rss.validSampleCount) &&
rss.validSampleCount > 0
) {
validMeasuredRunCount += 1;
continue;
}
invalidMeasuredRuns.push({
missingSampleCount: rss.missingSampleCount,
reason: rss.unavailableReason ?? 'renderer-rss-capture-invalid',
runId: iteration.runId,
validSampleCount: rss.validSampleCount,
});
}
const valid =
measured.length > 0 && validMeasuredRunCount === measured.length;
return Object.freeze({
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
validForBenchmark: valid,
validForComparison: valid,
validMeasuredRunCount,
});
}
function derivePhases(
main: MainCaptureMetrics,
renderer: RendererCaptureMetrics
@@ -1,5 +1,9 @@
/* eslint-disable max-lines -- The injected main-process protocol must remain self-contained for Playwright serialization. */
import type { ElectronApplication } from '@playwright/test';
import {
M3U_IMPORT_PERFORMANCE_PHASE,
M3U_IMPORT_PERFORMANCE_PHASE_EVENT_CHANNEL,
} from '@iptvnator/shared/interfaces';
import {
DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY,
@@ -58,6 +62,7 @@ export interface MainCaptureStartOptions {
}
export interface MainCaptureStatus {
readonly databaseGetsCompleted: number;
readonly databasePending: number;
readonly databaseRequests: number;
readonly databaseUpsertsCompleted: number;
@@ -65,6 +70,7 @@ export interface MainCaptureStatus {
readonly playlistResponsesFailed: number;
readonly playlistResponses: number;
readonly playlistResponsesSucceeded: number;
readonly preloadSuccessMarkers: number;
}
export interface MainCaptureRolloverResult {
@@ -101,6 +107,9 @@ export async function installMainCapture(
createRendererProcessRssCaptureApi.toString(),
rendererWindowRssSessionApiFactorySource:
createRendererWindowRssSessionApi.toString(),
m3uImportPerformancePhaseEventChannel:
M3U_IMPORT_PERFORMANCE_PHASE_EVENT_CHANNEL,
m3uImportPerformancePhases: Object.values(M3U_IMPORT_PERFORMANCE_PHASE),
stateKey: MAIN_CAPTURE_STATE_KEY,
workerTerminationGenerationApiFactorySource:
createWorkerTerminationGenerationApi.toString(),
@@ -158,12 +167,14 @@ export async function installMainCapture(
cpuLast: WorkerCpuUsage | null;
elu: number | null;
eluStart: WorkerElu | null;
externalMemorySampleCount: number;
externalPeak: number;
finalized: boolean;
finalizationKey: object;
finalizationTimedOut: boolean;
finalizing: Promise<void> | null;
heapPeak: number;
heapUsedSampleCount: number;
kind: 'database.worker' | 'playlist-refresh.worker';
operationId: string | null;
ordinal: number;
@@ -185,12 +196,19 @@ export async function installMainCapture(
worker: InstrumentedWorker;
}
interface TimelineRecord {
readonly boundary?: 'end' | 'start';
readonly byteCount?: number;
readonly durationMs?: number | null;
readonly epochMs: number;
readonly ipcCallId?: number;
readonly itemCount?: number;
readonly operation?: string;
readonly operationId?: string;
readonly phase?: string;
readonly playlistId?: string;
readonly requestId?: string;
readonly success?: boolean;
readonly sourceEpochMs?: number;
readonly type: string;
}
@@ -245,6 +263,9 @@ export async function installMainCapture(
const inspector = runtimeProcess.getBuiltinModule(
'node:inspector'
) as typeof import('node:inspector');
const diagnosticsChannel = runtimeProcess.getBuiltinModule(
'node:diagnostics_channel'
) as typeof import('node:diagnostics_channel');
const path = runtimeProcess.getBuiltinModule(
'node:path'
) as typeof import('node:path');
@@ -307,6 +328,60 @@ export async function installMainCapture(
state.timeline.push({ epochMs: nowEpochMs(), ...record });
}
};
const m3uImportPhases = new Set<string>(
input.m3uImportPerformancePhases
);
diagnosticsChannel
.channel(input.m3uImportPerformancePhaseEventChannel)
.subscribe((incoming: unknown) => {
if (!state.active && !state.stopping) {
return;
}
if (typeof incoming !== 'object' || incoming === null) {
return;
}
const event = incoming as JsonRecord;
const boundary = event['boundary'];
const durationMs = event['durationMs'];
const epochMs = event['epochMs'];
const metadata =
typeof event['metadata'] === 'object' &&
event['metadata'] !== null
? (event['metadata'] as JsonRecord)
: null;
const phase = event['phase'];
const requestId = event['requestId'];
if (
(boundary !== 'start' && boundary !== 'end') ||
typeof epochMs !== 'number' ||
!Number.isFinite(epochMs) ||
typeof phase !== 'string' ||
!m3uImportPhases.has(phase) ||
typeof requestId !== 'string' ||
requestId.length === 0 ||
(boundary === 'start' && durationMs !== null) ||
(boundary === 'end' &&
(typeof durationMs !== 'number' ||
!Number.isFinite(durationMs) ||
durationMs < 0))
) {
return;
}
const byteCount = metadata?.['byteCount'];
const itemCount = metadata?.['itemCount'];
state.timeline.push({
boundary,
...(isCount(byteCount) ? { byteCount } : {}),
durationMs: durationMs as number | null,
epochMs,
...(isCount(itemCount) ? { itemCount } : {}),
phase,
requestId,
type: 'm3u-import-phase',
});
});
const isCount = (value: unknown): value is number =>
Number.isSafeInteger(value) && Number(value) >= 0;
const classifyRequest = (
message: JsonRecord
): 'database.worker' | 'playlist-refresh.worker' | null => {
@@ -348,12 +423,14 @@ export async function installMainCapture(
cpuLast: null,
elu: null,
eluStart: null,
externalMemorySampleCount: 0,
externalPeak: 0,
finalized: false,
finalizationKey: {},
finalizationTimedOut: false,
finalizing: null,
heapPeak: 0,
heapUsedSampleCount: 0,
kind,
operationId: null,
ordinal: nextWorkerOrdinal,
@@ -400,8 +477,10 @@ export async function installMainCapture(
record.responseEpochMs = responseEpochMs;
record.requestPerformance.push({
identity: {
ipcCallId: null,
operationId: record.operationId,
operationIdUnavailableReason: null,
sourceEpochMs: null,
},
operation: null,
performanceCapture: message['performance'] ?? null,
@@ -466,14 +545,30 @@ export async function installMainCapture(
try {
const stats = await record.worker.getHeapStatistics?.();
if (stats) {
record.heapPeak = Math.max(
record.heapPeak,
Number(stats.used_heap_size ?? 0)
);
record.externalPeak = Math.max(
record.externalPeak,
Number(stats.external_memory ?? 0)
);
const heapUsedSample = stats.used_heap_size;
if (
typeof heapUsedSample === 'number' &&
Number.isFinite(heapUsedSample) &&
heapUsedSample >= 0
) {
record.heapUsedSampleCount += 1;
record.heapPeak = Math.max(
record.heapPeak,
heapUsedSample
);
}
const externalMemorySample = stats.external_memory;
if (
typeof externalMemorySample === 'number' &&
Number.isFinite(externalMemorySample) &&
externalMemorySample >= 0
) {
record.externalMemorySampleCount += 1;
record.externalPeak = Math.max(
record.externalPeak,
externalMemorySample
);
}
}
const cpu = await record.worker.cpuUsage?.();
if (cpu) {
@@ -504,12 +599,14 @@ export async function installMainCapture(
record.cpuLast = null;
record.elu = null;
record.eluStart = null;
record.externalMemorySampleCount = 0;
record.externalPeak = 0;
record.finalized = false;
record.finalizationKey = {};
record.finalizationTimedOut = false;
record.finalizing = null;
record.heapPeak = 0;
record.heapUsedSampleCount = 0;
record.operationId = null;
record.pendingCount = 0;
record.playlistId = null;
@@ -863,8 +960,10 @@ export async function installMainCapture(
value
)
: {
ipcCallId: null,
operationId: payloadOperationId,
operationIdUnavailableReason: null,
sourceEpochMs: null,
};
record.pendingCount += 1;
dbRequests.set(value['requestId'], {
@@ -874,11 +973,14 @@ export async function installMainCapture(
record,
});
recordTimeline({
ipcCallId: identity.ipcCallId ?? undefined,
operation: value['operation'],
operationId: identity.operationId ?? undefined,
playlistId:
readDatabasePlaylistId(value) ?? undefined,
requestId: value['requestId'],
sourceEpochMs:
identity.sourceEpochMs ?? undefined,
type: 'db-request',
});
}
@@ -1052,6 +1154,12 @@ export async function installMainCapture(
const api = {
status: (): MainCaptureStatus => ({
databaseGetsCompleted: state.timeline.filter(
(entry) =>
entry['type'] === 'db-response' &&
entry['operation'] === 'DB_GET_APP_PLAYLIST' &&
entry['success'] === true
).length,
databasePending: dbRequests.size,
databaseRequests: state.timeline.filter(
(entry) => entry['type'] === 'db-request'
@@ -1081,6 +1189,8 @@ export async function installMainCapture(
entry['type'] === 'playlist-response' &&
entry['success'] === true
).length,
preloadSuccessMarkers:
databaseRequestIdentityCapture.successMarkerCount(),
}),
start: async (options: MainCaptureStartOptions): Promise<void> => {
databaseWorkerPostGcCutoffApi.beginCapture();
@@ -1092,6 +1202,18 @@ export async function installMainCapture(
databaseWorkerPostGcCutoffApi.beginStop();
state.stopping = true;
state.active = false;
for (const marker of databaseRequestIdentityCapture.takeSuccessMarkers()) {
state.timeline.push({
epochMs: marker.sourceEpochMs,
ipcCallId: marker.ipcCallId,
operation: marker.operation,
operationId: marker.operationId ?? undefined,
playlistId: marker.playlistId,
sourceEpochMs: marker.sourceEpochMs,
success: true,
type: 'preload-performance-success',
});
}
databaseRequestIdentityCapture.stop();
if (state.sampleTimer) {
clearInterval(state.sampleTimer);
@@ -1230,11 +1352,28 @@ export async function installMainCapture(
? record.cpuLast.user - record.cpuFirst.user
: null,
eventLoopUtilization: record.elu,
externalMemorySampleCount:
record.externalMemorySampleCount,
heapUsedSampleCount: record.heapUsedSampleCount,
kind: record.kind,
operationId: record.operationId,
ordinal: record.ordinal,
peakExternalBytes: record.externalPeak,
peakHeapUsedBytes: record.heapPeak,
peakExternalBytes:
record.externalMemorySampleCount > 0
? record.externalPeak
: null,
peakExternalUnavailableReason:
record.externalMemorySampleCount > 0
? null
: 'worker-external-memory-samples-missing',
peakHeapUsedBytes:
record.heapUsedSampleCount > 0
? record.heapPeak
: null,
peakHeapUsedUnavailableReason:
record.heapUsedSampleCount > 0
? null
: 'worker-heap-used-samples-missing',
playlistId: record.playlistId,
postGcHeapUnavailableReason:
record.postGcHeapUnavailableReason,
@@ -1245,6 +1384,7 @@ export async function installMainCapture(
terminatedEpochMs: record.terminatedEpochMs,
},
requests: record.requestPerformance.map((request) => ({
ipcCallId: request.identity.ipcCallId,
operation: request.operation,
operationId: request.identity.operationId,
operationIdUnavailableReason:
@@ -1253,6 +1393,7 @@ export async function installMainCapture(
playlistId: request.playlistId,
requestId: request.requestId,
responseEpochMs: request.responseEpochMs,
sourceEpochMs: request.identity.sourceEpochMs,
success: request.success,
})),
}));
@@ -270,3 +270,34 @@ test('the cancellation benchmark exposes GC to Electron worker isolates', () =>
assert.match(source, /'--js-flags=--expose-gc'/);
assert.doesNotMatch(source, /execArgv/);
});
test('the initial M3U import benchmark uses the production-equivalent performance build', () => {
const target = e2eProject.targets['benchmark-m3u-import'];
assert.ok(target, 'electron-backend-e2e must define benchmark-m3u-import');
assert.deepEqual(target.dependsOn, ['electron-backend:build-performance']);
});
test('the initial M3U import benchmark command is pinned to its Playwright test file', () => {
const target = e2eProject.targets['benchmark-m3u-import'];
assert.equal(
target.options?.['command'],
'pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-import.performance.ts'
);
});
test('the initial M3U import benchmark enables opt-in capture and worker GC', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-import.benchmark.ts'
),
'utf8'
);
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
assert.match(source, /IPTVNATOR_PERF_WORKER_PROFILING:\s*'1'/);
assert.match(source, /'--js-flags=--expose-gc'/);
assert.doesNotMatch(source, /execArgv/);
});
@@ -0,0 +1,158 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
pairPerformancePhaseEvents,
requirePerformancePhase,
} from './performance-phase-events';
describe('performance phase event pairing', () => {
it('pairs request-scoped boundaries and preserves end metadata', () => {
const phases = pairPerformancePhaseEvents([
{
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'data.acquire',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 12.5,
epochMs: 113,
metadata: { byteCount: 2048, itemCount: 10 },
phase: 'data.acquire',
requestId: 'request-1',
},
]);
assert.deepEqual(phases, [
{
durationMs: 12.5,
endedEpochMs: 113,
metadata: { byteCount: 2048, itemCount: 10 },
phase: 'data.acquire',
requestId: 'request-1',
startedEpochMs: 100,
},
]);
assert.deepEqual(
requirePerformancePhase(phases, 'request-1', 'data.acquire'),
phases[0]
);
});
it('pairs interleaved requests without cross-correlating them', () => {
const phases = pairPerformancePhaseEvents([
{
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'serialize.playlist',
requestId: 'request-a',
},
{
boundary: 'start',
durationMs: null,
epochMs: 101,
phase: 'serialize.playlist',
requestId: 'request-b',
},
{
boundary: 'end',
durationMs: 3,
epochMs: 104,
phase: 'serialize.playlist',
requestId: 'request-b',
},
{
boundary: 'end',
durationMs: 8,
epochMs: 108,
phase: 'serialize.playlist',
requestId: 'request-a',
},
]);
assert.deepEqual(
phases.map(({ durationMs, requestId }) => ({
durationMs,
requestId,
})),
[
{ durationMs: 8, requestId: 'request-a' },
{ durationMs: 3, requestId: 'request-b' },
]
);
});
it('fails closed for orphaned, duplicate, malformed, or reversed boundaries', () => {
const validStart = {
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'normalize',
requestId: 'request-1',
} as const;
const validEnd = {
boundary: 'end',
durationMs: 2,
epochMs: 102,
phase: 'normalize',
requestId: 'request-1',
} as const;
for (const invalid of [
[validStart],
[validStart, validStart, validEnd],
[validStart, { ...validEnd, durationMs: null }],
[validStart, { ...validEnd, epochMs: 99 }],
[{ ...validStart, durationMs: 0 }, validEnd],
[
validStart,
{
...validEnd,
metadata: { byteCount: -1 },
},
],
]) {
assert.throws(
() => pairPerformancePhaseEvents(invalid),
/performance-phase-events-invalid/
);
}
});
it('requires one exact request and phase match', () => {
const phases = pairPerformancePhaseEvents([
{
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'normalize',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 2,
epochMs: 102,
phase: 'normalize',
requestId: 'request-1',
},
]);
assert.throws(
() => requirePerformancePhase(phases, 'request-2', 'normalize'),
/performance-phase-missing:request-2:normalize/
);
assert.throws(
() =>
requirePerformancePhase(
[...phases, ...phases],
'request-1',
'normalize'
),
/performance-phase-ambiguous:request-1:normalize/
);
});
});
@@ -0,0 +1,166 @@
import type { PerformancePhaseMetadata } from '@iptvnator/shared/interfaces';
export interface PairedPerformancePhase {
readonly durationMs: number;
readonly endedEpochMs: number;
readonly metadata: PerformancePhaseMetadata | null;
readonly phase: string;
readonly requestId: string;
readonly startedEpochMs: number;
}
interface ParsedPerformancePhaseEvent {
readonly boundary: 'end' | 'start';
readonly durationMs: number | null;
readonly epochMs: number;
readonly metadata: PerformancePhaseMetadata | null;
readonly phase: string;
readonly requestId: string;
}
export function pairPerformancePhaseEvents(
input: readonly unknown[]
): readonly PairedPerformancePhase[] {
const grouped = new Map<string, ParsedPerformancePhaseEvent[]>();
for (const value of input) {
const event = parsePerformancePhaseEvent(value);
const key = `${event.requestId}\u0000${event.phase}`;
const events = grouped.get(key) ?? [];
events.push(event);
if (events.length > 2) {
throw invalidPhaseEvents();
}
grouped.set(key, events);
}
const paired: PairedPerformancePhase[] = [];
for (const events of grouped.values()) {
const start = events.find((event) => event.boundary === 'start');
const end = events.find((event) => event.boundary === 'end');
if (
events.length !== 2 ||
!start ||
!end ||
start.durationMs !== null ||
start.metadata !== null ||
end.durationMs === null ||
end.epochMs < start.epochMs
) {
throw invalidPhaseEvents();
}
paired.push(
Object.freeze({
durationMs: end.durationMs,
endedEpochMs: end.epochMs,
metadata: end.metadata,
phase: start.phase,
requestId: start.requestId,
startedEpochMs: start.epochMs,
})
);
}
paired.sort(
(left, right) =>
left.startedEpochMs - right.startedEpochMs ||
left.endedEpochMs - right.endedEpochMs ||
left.requestId.localeCompare(right.requestId) ||
left.phase.localeCompare(right.phase)
);
return Object.freeze(paired);
}
export function requirePerformancePhase(
phases: readonly PairedPerformancePhase[],
requestId: string,
phase: string
): PairedPerformancePhase {
const matches = phases.filter(
(candidate) =>
candidate.requestId === requestId && candidate.phase === phase
);
if (matches.length === 0) {
throw new Error(`performance-phase-missing:${requestId}:${phase}`);
}
if (matches.length !== 1 || !matches[0]) {
throw new Error(`performance-phase-ambiguous:${requestId}:${phase}`);
}
return matches[0];
}
function parsePerformancePhaseEvent(
value: unknown
): ParsedPerformancePhaseEvent {
if (!isRecord(value)) {
throw invalidPhaseEvents();
}
const boundary = value['boundary'];
const durationMs = value['durationMs'];
const epochMs = value['epochMs'];
const phase = value['phase'];
const requestId = value['requestId'];
if (
(boundary !== 'start' && boundary !== 'end') ||
(durationMs !== null && !isFiniteNonNegativeNumber(durationMs)) ||
!isFiniteNonNegativeNumber(epochMs) ||
typeof phase !== 'string' ||
phase.length === 0 ||
typeof requestId !== 'string' ||
requestId.length === 0
) {
throw invalidPhaseEvents();
}
return {
boundary,
durationMs,
epochMs,
metadata: parseMetadata(value['metadata']),
phase,
requestId,
};
}
function parseMetadata(value: unknown): PerformancePhaseMetadata | null {
if (value === undefined) {
return null;
}
if (!isRecord(value)) {
throw invalidPhaseEvents();
}
const keys = Object.keys(value);
if (
keys.some((key) => key !== 'byteCount' && key !== 'itemCount') ||
keys.some((key) => !isSafeCount(value[key]))
) {
throw invalidPhaseEvents();
}
return Object.freeze({
...(value['byteCount'] === undefined
? {}
: { byteCount: Number(value['byteCount']) }),
...(value['itemCount'] === undefined
? {}
: { itemCount: Number(value['itemCount']) }),
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFiniteNonNegativeNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function isSafeCount(value: unknown): boolean {
return (
value === undefined ||
(Number.isSafeInteger(value) && Number(value) >= 0)
);
}
function invalidPhaseEvents(): Error {
return new Error('performance-phase-events-invalid');
}
@@ -0,0 +1,133 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
pairRendererPerformancePhaseEvents,
requireRendererPerformancePhase,
} from './renderer-performance-phase-events';
describe('renderer performance phase event pairing', () => {
it('pairs a successful renderer phase using monotonic duration', () => {
const phases = pairRendererPerformancePhaseEvents([
{
boundary: 'start',
epochMs: 100,
metadata: { items: 10_000 },
monotonicMs: 20,
phase: 'store.m3u-publish-channels',
phaseId: 'phase-1',
schemaVersion: 1,
},
{
boundary: 'end',
epochMs: 106,
metadata: { items: 10_000 },
monotonicMs: 25.5,
outcome: 'success',
phase: 'store.m3u-publish-channels',
phaseId: 'phase-1',
schemaVersion: 1,
},
]);
assert.deepEqual(phases, [
{
durationMs: 5.5,
endedEpochMs: 106,
metadata: { items: 10_000 },
outcome: 'success',
phase: 'store.m3u-publish-channels',
phaseId: 'phase-1',
startedEpochMs: 100,
},
]);
assert.deepEqual(
requireRendererPerformancePhase(
phases,
'store.m3u-publish-channels'
),
phases[0]
);
});
it('fails closed for inconsistent identity, metadata, outcome, and clocks', () => {
const start = {
boundary: 'start',
epochMs: 100,
metadata: { items: 50_000 },
monotonicMs: 20,
phase: 'store.m3u-import-dispatch',
phaseId: 'phase-1',
schemaVersion: 1,
} as const;
const end = {
boundary: 'end',
epochMs: 102,
metadata: { items: 50_000 },
monotonicMs: 22,
outcome: 'success',
phase: 'store.m3u-import-dispatch',
phaseId: 'phase-1',
schemaVersion: 1,
} as const;
for (const invalid of [
[start],
[start, start, end],
[start, { ...end, phase: 'store.m3u-publish-channels' }],
[start, { ...end, metadata: { items: 10_000 } }],
[{ ...start, outcome: 'success' }, end],
[start, { ...end, outcome: undefined }],
[start, { ...end, epochMs: 99 }],
[start, { ...end, monotonicMs: 19 }],
[start, { ...end, metadata: { items: -1 } }],
]) {
assert.throws(
() => pairRendererPerformancePhaseEvents(invalid),
/renderer-performance-phase-events-invalid/
);
}
});
it('requires exactly one successful event for a named phase', () => {
const successful = {
durationMs: 2,
endedEpochMs: 102,
metadata: { items: 10_000 },
outcome: 'success',
phase: 'store.m3u-import-dispatch',
phaseId: 'phase-1',
startedEpochMs: 100,
} as const;
const failed = {
...successful,
outcome: 'error',
phaseId: 'phase-2',
} as const;
assert.throws(
() =>
requireRendererPerformancePhase(
[successful],
'store.m3u-publish-channels'
),
/renderer-performance-phase-missing:store.m3u-publish-channels/
);
assert.throws(
() =>
requireRendererPerformancePhase(
[successful, successful],
'store.m3u-import-dispatch'
),
/renderer-performance-phase-ambiguous:store.m3u-import-dispatch/
);
assert.throws(
() =>
requireRendererPerformancePhase(
[failed],
'store.m3u-import-dispatch'
),
/renderer-performance-phase-failed:store.m3u-import-dispatch/
);
});
});
@@ -0,0 +1,163 @@
export interface PairedRendererPerformancePhase {
readonly durationMs: number;
readonly endedEpochMs: number;
readonly metadata: Readonly<{ items: number }> | null;
readonly outcome: 'error' | 'success';
readonly phase: string;
readonly phaseId: string;
readonly startedEpochMs: number;
}
interface ParsedRendererPerformancePhaseEvent {
readonly boundary: 'end' | 'start';
readonly epochMs: number;
readonly metadata: Readonly<{ items: number }> | null;
readonly monotonicMs: number;
readonly outcome: 'error' | 'success' | null;
readonly phase: string;
readonly phaseId: string;
}
export function pairRendererPerformancePhaseEvents(
input: readonly unknown[]
): readonly PairedRendererPerformancePhase[] {
const grouped = new Map<string, ParsedRendererPerformancePhaseEvent[]>();
for (const value of input) {
const event = parseRendererPerformancePhaseEvent(value);
const events = grouped.get(event.phaseId) ?? [];
events.push(event);
if (events.length > 2) {
throw invalidRendererEvents();
}
grouped.set(event.phaseId, events);
}
const paired: PairedRendererPerformancePhase[] = [];
for (const events of grouped.values()) {
const start = events.find((event) => event.boundary === 'start');
const end = events.find((event) => event.boundary === 'end');
if (
events.length !== 2 ||
!start ||
!end ||
start.outcome !== null ||
end.outcome === null ||
start.phase !== end.phase ||
!sameMetadata(start.metadata, end.metadata) ||
end.epochMs < start.epochMs ||
end.monotonicMs < start.monotonicMs
) {
throw invalidRendererEvents();
}
paired.push(
Object.freeze({
durationMs: end.monotonicMs - start.monotonicMs,
endedEpochMs: end.epochMs,
metadata: end.metadata,
outcome: end.outcome,
phase: end.phase,
phaseId: end.phaseId,
startedEpochMs: start.epochMs,
})
);
}
paired.sort(
(left, right) =>
left.startedEpochMs - right.startedEpochMs ||
left.endedEpochMs - right.endedEpochMs ||
left.phaseId.localeCompare(right.phaseId)
);
return Object.freeze(paired);
}
export function requireRendererPerformancePhase(
phases: readonly PairedRendererPerformancePhase[],
phase: string
): PairedRendererPerformancePhase {
const matches = phases.filter((candidate) => candidate.phase === phase);
if (matches.length === 0) {
throw new Error(`renderer-performance-phase-missing:${phase}`);
}
if (matches.length !== 1 || !matches[0]) {
throw new Error(`renderer-performance-phase-ambiguous:${phase}`);
}
if (matches[0].outcome !== 'success') {
throw new Error(`renderer-performance-phase-failed:${phase}`);
}
return matches[0];
}
function parseRendererPerformancePhaseEvent(
value: unknown
): ParsedRendererPerformancePhaseEvent {
if (!isRecord(value) || value['schemaVersion'] !== 1) {
throw invalidRendererEvents();
}
const boundary = value['boundary'];
const epochMs = value['epochMs'];
const monotonicMs = value['monotonicMs'];
const outcome = value['outcome'];
const phase = value['phase'];
const phaseId = value['phaseId'];
if (
(boundary !== 'start' && boundary !== 'end') ||
!isFiniteNonNegativeNumber(epochMs) ||
!isFiniteNonNegativeNumber(monotonicMs) ||
typeof phase !== 'string' ||
phase.length === 0 ||
typeof phaseId !== 'string' ||
phaseId.length === 0 ||
(boundary === 'start' && outcome !== undefined) ||
(boundary === 'end' && outcome !== 'success' && outcome !== 'error')
) {
throw invalidRendererEvents();
}
return {
boundary,
epochMs,
metadata: parseMetadata(value['metadata']),
monotonicMs,
outcome: outcome === 'success' || outcome === 'error' ? outcome : null,
phase,
phaseId,
};
}
function parseMetadata(value: unknown): Readonly<{ items: number }> | null {
if (value === undefined) {
return null;
}
if (
!isRecord(value) ||
Object.keys(value).length !== 1 ||
!Number.isSafeInteger(value['items']) ||
Number(value['items']) < 0
) {
throw invalidRendererEvents();
}
return Object.freeze({ items: Number(value['items']) });
}
function sameMetadata(
left: Readonly<{ items: number }> | null,
right: Readonly<{ items: number }> | null
): boolean {
return left === null || right === null
? left === right
: left.items === right.items;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFiniteNonNegativeNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function invalidRendererEvents(): Error {
return new Error('renderer-performance-phase-events-invalid');
}
@@ -0,0 +1,172 @@
/* 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';
interface TestIteration {
readonly kind: 'diagnostic' | 'measured' | 'warmup';
readonly main: {
readonly rendererWindow: {
readonly rss: {
readonly identity: {
readonly creationTime: number;
readonly pid: number;
} | null;
readonly missingSampleCount: number;
readonly peakRssBytes: number | null;
readonly unavailableReason: string | null;
readonly validSampleCount: number;
};
};
};
readonly runId: string;
}
interface RendererRssValidity {
readonly invalidMeasuredRuns: readonly {
readonly missingSampleCount: number;
readonly reason: string;
readonly runId: string;
readonly validSampleCount: number;
}[];
readonly measuredRunCount: number;
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
interface RendererRssValidityModule {
assessRendererRssValidity?: (
iterations: readonly TestIteration[]
) => RendererRssValidity;
}
const validityModulePromise = import(
new URL('./renderer-rss-validity.ts', import.meta.url).href
)
.then((module) => module as RendererRssValidityModule)
.catch(() => null);
function iteration(
runId: string,
kind: TestIteration['kind'],
input: Partial<TestIteration['main']['rendererWindow']['rss']> = {}
): TestIteration {
return {
kind,
main: {
rendererWindow: {
rss: {
identity: {
creationTime: 1_721_234_567_890,
pid: 42,
},
missingSampleCount: 0,
peakRssBytes: 4_096,
unavailableReason: null,
validSampleCount: 3,
...input,
},
},
},
runId,
};
}
test('validates only measured renderer RSS runs and preserves the exact summary shape', async () => {
const module = await validityModulePromise;
assert.ok(module, 'generic renderer RSS validity helper must exist');
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
const result = assess([
iteration('warmup-invalid', 'warmup', { identity: null }),
iteration('run-01', 'measured'),
iteration('diagnostic-invalid', 'diagnostic', {
unavailableReason: 'renderer-process-gone',
}),
]);
assert.deepEqual(result, {
invalidMeasuredRuns: [],
measuredRunCount: 1,
validForBenchmark: true,
validForComparison: true,
validMeasuredRunCount: 1,
});
assert.ok(Object.isFrozen(result));
assert.ok(Object.isFrozen(result.invalidMeasuredRuns));
});
test('fails every incomplete measured capture without losing its raw reason and counts', async () => {
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
const result = assess([
iteration('run-no-identity', 'measured', { identity: null }),
iteration('run-missing-sample', 'measured', {
missingSampleCount: 1,
peakRssBytes: null,
unavailableReason: 'renderer-process-metric-missing-during-capture',
validSampleCount: 2,
}),
iteration('run-zero-peak', 'measured', { peakRssBytes: 0 }),
]);
assert.deepEqual(result, {
invalidMeasuredRuns: [
{
missingSampleCount: 0,
reason: 'renderer-rss-capture-invalid',
runId: 'run-no-identity',
validSampleCount: 3,
},
{
missingSampleCount: 1,
reason: 'renderer-process-metric-missing-during-capture',
runId: 'run-missing-sample',
validSampleCount: 2,
},
{
missingSampleCount: 0,
reason: 'renderer-rss-capture-invalid',
runId: 'run-zero-peak',
validSampleCount: 3,
},
],
measuredRunCount: 3,
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
});
test('requires at least one measured run', async () => {
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
assert.deepEqual(assess([iteration('warmup-01', 'warmup')]), {
invalidMeasuredRuns: [],
measuredRunCount: 0,
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
});
test('the cancellation report delegates renderer RSS validity to the generic helper', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation-report.ts', import.meta.url),
'utf8'
);
assert.match(
source,
/import \{ assessRendererRssValidity \} from '\.\/renderer-rss-validity';/
);
assert.doesNotMatch(source, /function assessRendererRssValidity\(/);
});
@@ -0,0 +1,72 @@
import type { RendererProcessRssCapture } from './renderer-process-rss-capture';
export interface RendererRssValidityIteration {
readonly kind: string;
readonly main: {
readonly rendererWindow: {
readonly rss: RendererProcessRssCapture;
};
};
readonly runId: string;
}
export interface InvalidRendererRssMeasuredRun {
readonly missingSampleCount: number;
readonly reason:
| Exclude<RendererProcessRssCapture['unavailableReason'], null>
| 'renderer-rss-capture-invalid';
readonly runId: string;
readonly validSampleCount: number;
}
export interface RendererRssValidity {
readonly invalidMeasuredRuns: readonly InvalidRendererRssMeasuredRun[];
readonly measuredRunCount: number;
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
export function assessRendererRssValidity(
iterations: readonly RendererRssValidityIteration[]
): RendererRssValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === 'measured'
);
const invalidMeasuredRuns: RendererRssValidity['invalidMeasuredRuns'][number][] =
[];
let validMeasuredRunCount = 0;
for (const iteration of measured) {
const rss = iteration.main.rendererWindow.rss;
if (
rss.identity !== null &&
Number.isSafeInteger(rss.peakRssBytes) &&
Number(rss.peakRssBytes) > 0 &&
rss.unavailableReason === null &&
rss.missingSampleCount === 0 &&
Number.isSafeInteger(rss.validSampleCount) &&
rss.validSampleCount > 0
) {
validMeasuredRunCount += 1;
continue;
}
invalidMeasuredRuns.push({
missingSampleCount: rss.missingSampleCount,
reason: rss.unavailableReason ?? 'renderer-rss-capture-invalid',
runId: iteration.runId,
validSampleCount: rss.validSampleCount,
});
}
const valid =
measured.length > 0 && validMeasuredRunCount === measured.length;
return Object.freeze({
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
validForBenchmark: valid,
validForComparison: valid,
validMeasuredRunCount,
});
}
@@ -0,0 +1,49 @@
/* eslint-disable playwright/expect-expect -- Node assertions define the loopback-only fixture server contract. */
import assert from 'node:assert/strict';
import test from 'node:test';
import { createSyntheticM3uFixture } from './synthetic-m3u';
import { startSyntheticM3uServer } from './synthetic-m3u-server';
test('serves and primes only the deterministic fixture on ephemeral IPv4 loopback', async () => {
const fixture = createSyntheticM3uFixture(10_000);
const server = await startSyntheticM3uServer(fixture);
try {
const url = new URL(server.resourceUrl);
assert.equal(url.protocol, 'http:');
assert.equal(url.hostname, '127.0.0.1');
assert.notEqual(url.port, '');
assert.equal(url.username, '');
assert.equal(url.password, '');
assert.equal(server.requestCount(), 0);
await server.prime();
assert.equal(server.requestCount(), 1);
const response = await fetch(server.resourceUrl);
assert.equal(response.status, 200);
assert.equal(
response.headers.get('content-length'),
String(fixture.bytes)
);
assert.equal(await response.text(), fixture.body);
assert.equal(server.requestCount(), 2);
const missing = await fetch(`${server.origin}/not-the-fixture.m3u`);
assert.equal(missing.status, 404);
assert.equal(server.requestCount(), 2);
} finally {
await server.close();
}
});
test('closes idempotently and rejects priming after close', async () => {
const server = await startSyntheticM3uServer(
createSyntheticM3uFixture(10_000)
);
await server.close();
await server.close();
await assert.rejects(server.prime(), /closed/i);
});
@@ -0,0 +1,114 @@
import { once } from 'node:events';
import { createServer, type Server } from 'node:http';
import type { SyntheticM3uFixture } from './synthetic-m3u';
export const SYNTHETIC_M3U_RESOURCE_PATH = '/synthetic-performance.m3u';
export interface SyntheticM3uServer {
readonly origin: string;
readonly resourceUrl: string;
close(): Promise<void>;
prime(): Promise<void>;
requestCount(): number;
}
export async function startSyntheticM3uServer(
fixture: SyntheticM3uFixture
): Promise<SyntheticM3uServer> {
let successfulRequestCount = 0;
const server = createServer((request, response) => {
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
if (
request.method !== 'GET' ||
requestUrl.pathname !== SYNTHETIC_M3U_RESOURCE_PATH ||
requestUrl.search.length > 0
) {
response.writeHead(404, {
'Content-Length': '0',
'Content-Type': 'text/plain; charset=utf-8',
});
response.end();
return;
}
successfulRequestCount += 1;
response.writeHead(200, {
'Cache-Control': 'no-store',
'Content-Length': String(fixture.bytes),
'Content-Type': 'application/vnd.apple.mpegurl; charset=utf-8',
});
response.end(fixture.body);
});
server.listen({
exclusive: true,
host: '127.0.0.1',
port: 0,
});
try {
await once(server, 'listening');
} catch (error: unknown) {
await closeServer(server).catch(() => undefined);
throw error;
}
const address = server.address();
if (
address === null ||
typeof address === 'string' ||
address.address !== '127.0.0.1' ||
!Number.isSafeInteger(address.port) ||
address.port <= 0
) {
await closeServer(server);
throw new Error(
'Synthetic M3U server did not bind exact IPv4 loopback'
);
}
const origin = `http://127.0.0.1:${address.port}`;
const resourceUrl = `${origin}${SYNTHETIC_M3U_RESOURCE_PATH}`;
let closePromise: Promise<void> | null = null;
return Object.freeze({
origin,
resourceUrl,
close: () => {
closePromise ??= closeServer(server);
return closePromise;
},
prime: async () => {
if (closePromise !== null) {
throw new Error('Synthetic M3U server is closed');
}
const response = await fetch(resourceUrl);
const body = await response.arrayBuffer();
if (
response.status !== 200 ||
Number(response.headers.get('content-length')) !==
fixture.bytes ||
body.byteLength !== fixture.bytes
) {
throw new Error(
'Synthetic M3U server priming response was invalid'
);
}
},
requestCount: () => successfulRequestCount,
});
}
async function closeServer(server: Server): Promise<void> {
if (!server.listening) {
return;
}
await new Promise<void>((resolvePromise, rejectPromise) => {
server.close((error) => {
if (error) {
rejectPromise(error);
return;
}
resolvePromise();
});
});
}
@@ -0,0 +1,68 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { describe, it } from 'node:test';
import {
createSyntheticM3uFixture,
SUPPORTED_SYNTHETIC_M3U_CHANNEL_COUNTS,
SYNTHETIC_M3U_SEED,
} from './synthetic-m3u';
const GOLDEN_SHA256 = {
10_000: '1d151ab2a878db8818f2bbfbdd689007cbe390182cdd1520c316bd163b7e7db3',
50_000: '4a5336a5d1c66b8b706752784815617a012499cb7a231ab35c88df9bd55ee78a',
100_000: '2ffe912f61ea106ba8de3414b0a927cdaa3264ac637d44c4b219f199eb94c353',
} as const;
describe('synthetic M3U performance fixture', () => {
it('generates each supported catalog deterministically with exact metadata', () => {
assert.deepEqual(
SUPPORTED_SYNTHETIC_M3U_CHANNEL_COUNTS,
[10_000, 50_000, 100_000]
);
assert.equal(SYNTHETIC_M3U_SEED, 240_724);
for (const channelCount of SUPPORTED_SYNTHETIC_M3U_CHANNEL_COUNTS) {
const fixture = createSyntheticM3uFixture(channelCount);
const lines = fixture.body.trimEnd().split('\n');
assert.equal(fixture.channelCount, channelCount);
assert.equal(
fixture.bytes,
Buffer.byteLength(fixture.body, 'utf8')
);
assert.equal(
fixture.sha256,
createHash('sha256').update(fixture.body, 'utf8').digest('hex')
);
assert.equal(fixture.sha256, GOLDEN_SHA256[channelCount]);
assert.equal(lines[0], '#EXTM3U');
assert.equal(lines.length, 1 + channelCount * 2);
assert.equal(
lines.filter((line) => line.startsWith('#EXTINF:')).length,
channelCount
);
assert.doesNotMatch(fixture.body, /\b(?:tvg-|logo|provider)\b/i);
assertOnlyLoopbackUrls(fixture.body);
}
});
it('rejects unsupported channel counts', () => {
assert.throws(
() => createSyntheticM3uFixture(9_999),
/Unsupported synthetic M3U channel count/
);
});
});
function assertOnlyLoopbackUrls(body: string): void {
const urls = body.match(/https?:\/\/[^\s"]+/g) ?? [];
for (const rawUrl of urls) {
const url = new URL(rawUrl);
assert.equal(url.protocol, 'http:');
assert.equal(url.hostname, '127.0.0.1');
assert.equal(url.username, '');
assert.equal(url.password, '');
}
}
@@ -2,19 +2,30 @@ import { createHash } from 'node:crypto';
export const SYNTHETIC_M3U_SEED = 240_724;
export const SYNTHETIC_M3U_CHANNEL_COUNT = 100_000;
export const SUPPORTED_SYNTHETIC_M3U_CHANNEL_COUNTS = [
10_000,
50_000,
SYNTHETIC_M3U_CHANNEL_COUNT,
] as const;
export type SyntheticM3uChannelCount =
(typeof SUPPORTED_SYNTHETIC_M3U_CHANNEL_COUNTS)[number];
export interface SyntheticM3uFixture {
readonly body: string;
readonly bytes: number;
readonly channelCount: number;
readonly channelCount: SyntheticM3uChannelCount;
readonly sha256: string;
}
export function createSyntheticM3uFixture(): SyntheticM3uFixture {
const lines = new Array<string>(1 + SYNTHETIC_M3U_CHANNEL_COUNT * 2);
export function createSyntheticM3uFixture(
channelCount: number = SYNTHETIC_M3U_CHANNEL_COUNT
): SyntheticM3uFixture {
assertSupportedChannelCount(channelCount);
const lines = new Array<string>(1 + channelCount * 2);
lines[0] = '#EXTM3U';
for (let offset = 0; offset < SYNTHETIC_M3U_CHANNEL_COUNT; offset += 1) {
for (let offset = 0; offset < channelCount; offset += 1) {
const index = offset + 1;
const group = (offset % 100) + 1;
const lineOffset = 1 + offset * 2;
@@ -34,7 +45,21 @@ export function createSyntheticM3uFixture(): SyntheticM3uFixture {
return Object.freeze({
body,
bytes: Buffer.byteLength(body, 'utf8'),
channelCount: SYNTHETIC_M3U_CHANNEL_COUNT,
channelCount,
sha256: createHash('sha256').update(body, 'utf8').digest('hex'),
});
}
function assertSupportedChannelCount(
channelCount: number
): asserts channelCount is SyntheticM3uChannelCount {
if (
!SUPPORTED_SYNTHETIC_M3U_CHANNEL_COUNTS.some(
(supported) => supported === channelCount
)
) {
throw new Error(
`Unsupported synthetic M3U channel count: ${channelCount}`
);
}
}
@@ -0,0 +1,149 @@
import {
APP_PLAYLIST_GET_PERFORMANCE_PHASE,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
type AppPlaylistPerformancePhase,
type PerformancePhaseEvent,
type PerformancePhaseMetadata,
} from '@iptvnator/shared/interfaces';
const PHASE_SEQUENCES = {
DB_GET_APP_PLAYLIST: [
APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.DESERIALIZE_PLAYLIST,
],
DB_UPSERT_APP_PLAYLIST: [
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE,
],
} as const satisfies Readonly<
Record<string, readonly AppPlaylistPerformancePhase[]>
>;
const EVENT_KEYS = new Set([
'boundary',
'durationMs',
'epochMs',
'metadata',
'phase',
'requestId',
]);
export function parseWorkerPerformancePhaseEvents(
operation: string | null,
input: unknown,
workStartedEpochMs: number,
workEndedEpochMs: number
): readonly PerformancePhaseEvent<AppPlaylistPerformancePhase>[] | null {
const phaseSequence = getPhaseSequence(operation);
if (phaseSequence.length === 0) {
return input === undefined ||
(Array.isArray(input) && input.length === 0)
? []
: null;
}
if (
!Array.isArray(input) ||
input.length !== phaseSequence.length * 2
) {
return null;
}
const parsed: PerformancePhaseEvent<AppPlaylistPerformancePhase>[] =
[];
let lastEpochMs = workStartedEpochMs;
for (let index = 0; index < input.length; index += 1) {
const raw = input[index];
if (
!isRecord(raw) ||
Object.keys(raw).some((key) => !EVENT_KEYS.has(key))
) {
return null;
}
const boundary = raw['boundary'];
const durationMs = raw['durationMs'];
const epochMs = raw['epochMs'];
const metadata = parseMetadata(raw['metadata']);
const phase = raw['phase'];
const requestId = raw['requestId'];
if (
boundary !== (index % 2 === 0 ? 'start' : 'end') ||
phase !== phaseSequence[Math.floor(index / 2)] ||
typeof requestId !== 'string' ||
requestId.length === 0 ||
!isFiniteNonNegativeNumber(epochMs) ||
epochMs < lastEpochMs ||
epochMs > workEndedEpochMs ||
(boundary === 'start' &&
(durationMs !== null || metadata !== undefined)) ||
(boundary === 'end' && !isFiniteNonNegativeNumber(durationMs)) ||
metadata === null ||
(parsed.length > 0 && parsed[0]?.requestId !== requestId)
) {
return null;
}
parsed.push({
boundary: boundary as 'end' | 'start',
durationMs: durationMs as number | null,
epochMs,
...(metadata === undefined ? {} : { metadata }),
phase: phase as AppPlaylistPerformancePhase,
requestId,
});
lastEpochMs = epochMs;
}
return parsed;
}
function getPhaseSequence(
operation: string | null
): readonly AppPlaylistPerformancePhase[] {
if (operation === 'DB_GET_APP_PLAYLIST') {
return PHASE_SEQUENCES.DB_GET_APP_PLAYLIST;
}
if (operation === 'DB_UPSERT_APP_PLAYLIST') {
return PHASE_SEQUENCES.DB_UPSERT_APP_PLAYLIST;
}
return [];
}
function parseMetadata(
input: unknown
): PerformancePhaseMetadata | undefined | null {
if (input === undefined) {
return undefined;
}
if (
!isRecord(input) ||
Object.keys(input).some(
(key) => key !== 'byteCount' && key !== 'itemCount'
)
) {
return null;
}
const byteCount = input['byteCount'];
const itemCount = input['itemCount'];
if (
(byteCount !== undefined && !isSafeCount(byteCount)) ||
(itemCount !== undefined && !isSafeCount(itemCount))
) {
return null;
}
return {
...(byteCount === undefined ? {} : { byteCount: Number(byteCount) }),
...(itemCount === undefined ? {} : { itemCount: Number(itemCount) }),
};
}
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === 'object' && input !== null && !Array.isArray(input);
}
function isFiniteNonNegativeNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function isSafeCount(value: unknown): boolean {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
@@ -13,6 +13,7 @@ const validCapture = {
histogramFlushedEpochMs: 1_050,
invalidReason: null,
requestReceivedEpochMs: 1_000,
responsePostedEpochMs: 1_055,
threadCpuSystemMicros: 50,
threadCpuUnavailableReason: null,
threadCpuUserMicros: 100,
@@ -20,19 +21,28 @@ const validCapture = {
workStartedEpochMs: 1_010,
} as const;
function normalize(performanceCapture: unknown) {
function normalizeForOperation(
operation: string,
performanceCapture: unknown
) {
return normalizeWorkerRequestPerformanceOutcome({
operation: 'DB_GET_APP_PLAYLIST',
ipcCallId: 2,
operation,
operationId: 'operation-42',
operationIdUnavailableReason: null,
performanceCapture,
playlistId: 'playlist-1',
requestId: 'request-1',
responseEpochMs: 1_060,
sourceEpochMs: 990,
success: true,
});
}
function normalize(performanceCapture: unknown) {
return normalizeForOperation('DB_GET_CONTENT', performanceCapture);
}
function assertClosedCapture(
capture: WorkerRequestPerformanceMetrics,
reason: string
@@ -47,6 +57,7 @@ function assertClosedCapture(
capture.histogramFlushedEpochMs,
capture.invalidReason,
capture.requestReceivedEpochMs,
capture.responsePostedEpochMs,
capture.threadCpuSystemMicros,
capture.threadCpuUnavailableReason,
capture.threadCpuUserMicros,
@@ -55,6 +66,7 @@ function assertClosedCapture(
]) {
assert.equal(value, null);
}
assert.deepEqual(capture.phaseEvents, []);
}
test('absent captures are missing while present non-record captures are invalid', () => {
@@ -118,6 +130,9 @@ test('malformed nested fields and cross-field invariants fail the whole capture
histogramFlushedEpochMs: null,
},
{ ...validCapture, histogramFlushedEpochMs: 1_039 },
{ ...validCapture, responsePostedEpochMs: undefined },
{ ...validCapture, responsePostedEpochMs: 1_039 },
{ ...validCapture, responsePostedEpochMs: 1_061 },
{ ...validCapture, workStartedEpochMs: 999 },
{
...validCapture,
@@ -170,3 +185,143 @@ test('coherent metric-unavailable and invalid worker results remain valid raw ca
assert.equal(legitimateInvalid.performanceCaptureUnavailableReason, null);
assert.equal(legitimateInvalid.invalidReason, invalidReason);
});
test('requires the exact operation-specific worker phase sequence', () => {
const upsertPhaseEvents = [
{
boundary: 'start',
durationMs: null,
epochMs: 1_015,
phase: 'serialize.playlist',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 5,
epochMs: 1_020,
metadata: { byteCount: 2048, itemCount: 10_000 },
phase: 'serialize.playlist',
requestId: 'request-1',
},
{
boundary: 'start',
durationMs: null,
epochMs: 1_021,
phase: 'sqlite.write',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 12,
epochMs: 1_033,
metadata: { byteCount: 2048, itemCount: 1 },
phase: 'sqlite.write',
requestId: 'request-1',
},
] as const;
const getPhaseEvents = [
{
boundary: 'start',
durationMs: null,
epochMs: 1_015,
phase: 'sqlite.read',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 5,
epochMs: 1_020,
metadata: { itemCount: 1 },
phase: 'sqlite.read',
requestId: 'request-1',
},
{
boundary: 'start',
durationMs: null,
epochMs: 1_021,
phase: 'deserialize.playlist',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 12,
epochMs: 1_033,
metadata: { itemCount: 10_000 },
phase: 'deserialize.playlist',
requestId: 'request-1',
},
] as const;
assert.deepEqual(
normalizeForOperation('DB_UPSERT_APP_PLAYLIST', {
...validCapture,
phaseEvents: upsertPhaseEvents,
}).phaseEvents,
upsertPhaseEvents
);
assert.deepEqual(
normalizeForOperation('DB_GET_APP_PLAYLIST', {
...validCapture,
phaseEvents: getPhaseEvents,
}).phaseEvents,
getPhaseEvents
);
for (const invalidPhaseEvents of [
getPhaseEvents.slice(0, 2),
getPhaseEvents.slice(1),
[...getPhaseEvents].reverse(),
getPhaseEvents.map((event) => ({
...event,
requestId: 'other-request',
})),
[
getPhaseEvents[0],
{
...getPhaseEvents[1],
metadata: { itemCount: 1, secret: 'not allowed' },
},
],
[
getPhaseEvents[0],
{
...getPhaseEvents[1],
phase: 'arbitrary.phase',
},
],
]) {
assertClosedCapture(
normalizeForOperation('DB_GET_APP_PLAYLIST', {
...validCapture,
phaseEvents: invalidPhaseEvents,
}),
'worker-performance-capture-invalid'
);
}
for (const [operation, phaseEvents] of [
['DB_GET_APP_PLAYLIST', upsertPhaseEvents],
['DB_UPSERT_APP_PLAYLIST', getPhaseEvents],
['DB_GET_CONTENT', getPhaseEvents],
] as const) {
assertClosedCapture(
normalizeForOperation(operation, {
...validCapture,
phaseEvents,
}),
'worker-performance-capture-invalid'
);
}
for (const operation of [
'DB_GET_APP_PLAYLIST',
'DB_UPSERT_APP_PLAYLIST',
]) {
assertClosedCapture(
normalizeForOperation(operation, validCapture),
'worker-performance-capture-invalid'
);
}
assert.deepEqual(normalize(validCapture).phaseEvents, []);
});
@@ -35,9 +35,10 @@ function requestPerformance(
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: p95Ms / 100,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 1_050 + p95Ms,
histogramFlushedEpochMs: 1_050,
invalidReason: null,
requestReceivedEpochMs: 1_000,
responsePostedEpochMs: 1_055,
threadCpuSystemMicros: threadCpuUserMicros / 2,
threadCpuUnavailableReason: null,
threadCpuUserMicros,
@@ -53,13 +54,15 @@ function requestTransport(
requestId = 'request-1'
) {
return {
operation: 'DB_GET_APP_PLAYLIST',
ipcCallId: 7,
operation: 'DB_GET_CONTENT',
operationId: 'operation-42',
operationIdUnavailableReason: null,
performanceCapture,
playlistId: 'playlist-1',
requestId,
responseEpochMs: 1_060,
sourceEpochMs: 990,
success: true,
} as const;
}
@@ -77,6 +80,7 @@ function assertClosedCapture(
capture.histogramFlushedEpochMs,
capture.invalidReason,
capture.requestReceivedEpochMs,
capture.responsePostedEpochMs,
capture.threadCpuSystemMicros,
capture.threadCpuUnavailableReason,
capture.threadCpuUserMicros,
@@ -220,8 +224,10 @@ test('main capture retains raw request identity, timestamps, metrics, and reason
assert.match(source, /requestPerformance: \[\]/);
assert.match(source, /record\.requestPerformance\.map/);
assert.match(source, /operationId: request\.identity\.operationId/);
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, /captureGeneration: state\.captureGeneration/);
assert.doesNotMatch(
source,
@@ -229,6 +235,13 @@ test('main capture retains raw request identity, timestamps, metrics, and reason
);
});
test('normalized raw request metrics retain the exact preload IPC call identity', () => {
const capture = requestPerformance('request-1', 5, 100);
assert.equal(capture.ipcCallId, 7);
assert.equal(capture.sourceEpochMs, 990);
});
test('benchmark resolves one exact BrowserWindow and main capture never scans all windows', () => {
const benchmarkSource = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
@@ -6,8 +6,10 @@ import type {
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import { WORKER_POST_GC_HEAP_UNAVAILABLE_REASON } from './m3u-refresh-cancellation-contract';
import { parseWorkerPerformancePhaseEvents } from './worker-performance-phase-events';
export interface WorkerRequestPerformanceOutcomeTransport {
readonly ipcCallId: number | null;
readonly operation: string | null;
readonly operationId: string | null;
readonly operationIdUnavailableReason: string | null;
@@ -15,6 +17,7 @@ export interface WorkerRequestPerformanceOutcomeTransport {
readonly playlistId: string | null;
readonly requestId: string | null;
readonly responseEpochMs: number;
readonly sourceEpochMs: number | null;
readonly success: boolean;
}
@@ -39,7 +42,9 @@ type ParsedWorkerPerformanceCapture = Pick<
| 'eventLoopUtilizationUnavailableReason'
| 'histogramFlushedEpochMs'
| 'invalidReason'
| 'phaseEvents'
| 'requestReceivedEpochMs'
| 'responsePostedEpochMs'
| 'threadCpuSystemMicros'
| 'threadCpuUnavailableReason'
| 'threadCpuUserMicros'
@@ -146,7 +151,8 @@ function parseEventLoopDelay(
}
function parseWorkerPerformanceCapture(
input: unknown
input: unknown,
operation: string | null
): ParsedWorkerPerformanceCapture | null {
if (!isRecord(input)) {
return null;
@@ -161,6 +167,7 @@ function parseWorkerPerformanceCapture(
const histogramFlushedEpochMs = input['histogramFlushedEpochMs'];
const invalidReason = input['invalidReason'];
const requestReceivedEpochMs = input['requestReceivedEpochMs'];
const responsePostedEpochMs = input['responsePostedEpochMs'];
const threadCpuSystemMicros = input['threadCpuSystemMicros'];
const threadCpuUnavailableReason = input['threadCpuUnavailableReason'];
const threadCpuUserMicros = input['threadCpuUserMicros'];
@@ -182,6 +189,7 @@ function parseWorkerPerformanceCapture(
!isNullableFiniteNonNegativeNumber(histogramFlushedEpochMs) ||
!isNullableReason(invalidReason, INVALID_REASONS) ||
!isFiniteNonNegativeNumber(requestReceivedEpochMs) ||
!isFiniteNonNegativeNumber(responsePostedEpochMs) ||
!isNullableFiniteNonNegativeNumber(threadCpuSystemMicros) ||
!isNullableReason(threadCpuUnavailableReason, THREAD_CPU_REASONS) ||
!isNullableFiniteNonNegativeNumber(threadCpuUserMicros) ||
@@ -191,11 +199,23 @@ function parseWorkerPerformanceCapture(
return null;
}
const phaseEvents = parseWorkerPerformancePhaseEvents(
operation,
input['phaseEvents'],
workStartedEpochMs,
workEndedEpochMs
);
if (phaseEvents === null) {
return null;
}
const timestampsAreOrdered =
requestReceivedEpochMs <= workStartedEpochMs &&
workStartedEpochMs <= workEndedEpochMs &&
workEndedEpochMs <= responsePostedEpochMs &&
(histogramFlushedEpochMs === null ||
workEndedEpochMs <= histogramFlushedEpochMs);
(workEndedEpochMs <= histogramFlushedEpochMs &&
histogramFlushedEpochMs <= responsePostedEpochMs));
const eventLoopDelayIsCoherent =
eventLoopDelay === null
? eventLoopDelayUnavailableReason !== null &&
@@ -241,7 +261,9 @@ function parseWorkerPerformanceCapture(
eventLoopUtilizationUnavailableReason,
histogramFlushedEpochMs,
invalidReason,
phaseEvents,
requestReceivedEpochMs,
responsePostedEpochMs,
threadCpuSystemMicros,
threadCpuUnavailableReason,
threadCpuUserMicros,
@@ -254,12 +276,14 @@ export function normalizeWorkerRequestPerformanceOutcome(
input: WorkerRequestPerformanceOutcomeTransport
): WorkerRequestPerformanceMetrics {
const identity = {
ipcCallId: input.ipcCallId,
operation: input.operation,
operationId: input.operationId,
operationIdUnavailableReason: input.operationIdUnavailableReason,
playlistId: input.playlistId,
requestId: input.requestId,
responseEpochMs: input.responseEpochMs,
sourceEpochMs: input.sourceEpochMs,
success: input.success,
};
const unavailable = (
@@ -275,7 +299,9 @@ export function normalizeWorkerRequestPerformanceOutcome(
invalidReason: null,
...identity,
performanceCaptureUnavailableReason: reason,
phaseEvents: [],
requestReceivedEpochMs: null,
responsePostedEpochMs: null,
threadCpuSystemMicros: null,
threadCpuUnavailableReason: null,
threadCpuUserMicros: null,
@@ -291,11 +317,27 @@ export function normalizeWorkerRequestPerformanceOutcome(
}
const performanceCapture = parseWorkerPerformanceCapture(
input.performanceCapture
input.performanceCapture,
input.operation
);
if (!performanceCapture) {
return unavailable('worker-performance-capture-invalid');
}
if (
!isFiniteNonNegativeNumber(input.responseEpochMs) ||
performanceCapture.responsePostedEpochMs > input.responseEpochMs
) {
return unavailable('worker-performance-capture-invalid');
}
if (
performanceCapture.phaseEvents.length > 0 &&
(input.requestId === null ||
performanceCapture.phaseEvents.some(
(event) => event.requestId !== input.requestId
))
) {
return unavailable('worker-performance-capture-invalid');
}
return {
...performanceCapture,
@@ -0,0 +1,123 @@
import type {
AppPlaylistGetPerformancePhase,
PerformancePhaseMetadata,
} from '@iptvnator/shared/interfaces';
import {
APP_PLAYLIST_GET_PERFORMANCE_PHASE,
} from '@iptvnator/shared/interfaces';
import type { AppDatabase } from '../database.types';
import { getAppPlaylist } from './playlist.operations';
type TestGetPhaseCapture = {
captureAsync: <TResult>(
phase: AppPlaylistGetPerformancePhase,
execute: () => Promise<TResult>,
metadata?: (result: TResult) => PerformancePhaseMetadata
) => Promise<TResult>;
captureSync: <TResult>(
phase: AppPlaylistGetPerformancePhase,
execute: () => TResult,
metadata?: (result: TResult) => PerformancePhaseMetadata
) => TResult;
};
function createDatabaseMock(rows: unknown[]) {
const limit = jest.fn().mockResolvedValue(rows);
const where = jest.fn(() => ({ limit }));
const from = jest.fn(() => ({ where }));
const select = jest.fn(() => ({ from }));
return {
db: { select } as unknown as AppDatabase,
from,
limit,
select,
where,
};
}
const playlistRow = {
id: 'synthetic-playlist',
name: 'Synthetic playlist',
type: 'm3u-url',
payload: JSON.stringify({
_id: 'synthetic-playlist',
playlist: {
items: [{ id: 'channel-1' }, { id: 'channel-2' }],
},
title: 'Synthetic playlist',
}),
favorites: null,
recentlyViewed: null,
} as never;
describe('app playlist GET performance boundaries', () => {
it('keeps the default read and parse path unchanged', async () => {
const { db, limit, select } = createDatabaseMock([playlistRow]);
await expect(
getAppPlaylist(db, 'synthetic-playlist')
).resolves.toEqual(
expect.objectContaining({
_id: 'synthetic-playlist',
playlist: {
items: [{ id: 'channel-1' }, { id: 'channel-2' }],
},
})
);
expect(select).toHaveBeenCalledTimes(1);
expect(limit).toHaveBeenCalledWith(1);
});
it('separates the awaited SQLite read from playlist deserialization', async () => {
const { db, limit } = createDatabaseMock([playlistRow]);
const phases: Array<{
metadata: PerformancePhaseMetadata | undefined;
phase: AppPlaylistGetPerformancePhase;
}> = [];
const capture: TestGetPhaseCapture = {
async captureAsync(phase, execute, metadata) {
const result = await execute();
phases.push({
metadata: metadata?.(result),
phase,
});
return result;
},
captureSync(phase, execute, metadata) {
const result = execute();
phases.push({
metadata: metadata?.(result),
phase,
});
return result;
},
};
const getWithCapture = getAppPlaylist as unknown as (
database: AppDatabase,
playlistId: string,
phaseCapture: TestGetPhaseCapture
) => Promise<Record<string, unknown> | null>;
await expect(
getWithCapture(db, 'synthetic-playlist', capture)
).resolves.toEqual(
expect.objectContaining({
_id: 'synthetic-playlist',
})
);
expect(phases).toEqual([
{
metadata: { itemCount: 1 },
phase: APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
},
{
metadata: { itemCount: 2 },
phase: APP_PLAYLIST_GET_PERFORMANCE_PHASE.DESERIALIZE_PLAYLIST,
},
]);
expect(limit).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,101 @@
import {
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
type AppPlaylistUpsertPerformancePhase,
type PerformancePhaseMetadata,
} from '@iptvnator/shared/interfaces';
import type { AppDatabase } from '../database.types';
import {
type AppPlaylistUpsertPhaseCapture,
upsertAppPlaylist,
} from './playlist.operations';
function createDatabaseMock(runImplementation: () => unknown = () => ({})) {
const run = jest.fn(runImplementation);
const then = jest.fn(
(
onFulfilled: (value: unknown) => unknown,
onRejected: (error: unknown) => unknown
) => Promise.resolve().then(run).then(onFulfilled, onRejected)
);
const onConflictDoUpdate = jest.fn(() => ({ run, then }));
const values = jest.fn(() => ({ onConflictDoUpdate }));
const insert = jest.fn(() => ({ values }));
return {
db: { insert } as unknown as AppDatabase,
insert,
onConflictDoUpdate,
run,
values,
};
}
describe('app playlist upsert performance boundaries', () => {
const playlist = {
_id: 'synthetic-playlist',
playlist: {
items: [{ id: 'channel-1' }, { id: 'channel-2' }],
},
title: 'Synthetic playlist',
type: 'm3u-url',
url: 'http://127.0.0.1:43210/synthetic-performance.m3u',
};
afterEach(() => {
jest.restoreAllMocks();
});
it('keeps the default path free of phase hooks and executes one autocommit run', async () => {
const { db, run } = createDatabaseMock();
await expect(upsertAppPlaylist(db, playlist)).resolves.toEqual({
success: true,
});
expect(run).toHaveBeenCalledTimes(1);
});
it('separates row/JSON serialization from SQLite without rescanning the payload for metadata', async () => {
const { db, run } = createDatabaseMock();
const byteLength = jest.spyOn(Buffer, 'byteLength');
const phases: Array<{
metadata: PerformancePhaseMetadata | undefined;
phase: AppPlaylistUpsertPerformancePhase;
}> = [];
const capture: AppPlaylistUpsertPhaseCapture = {
async captureAsync(phase, execute, metadata) {
const result = await execute();
phases.push({
metadata: metadata?.(result),
phase,
});
return result;
},
captureSync(phase, execute, metadata) {
const result = execute();
phases.push({
metadata: metadata?.(result),
phase,
});
return result;
},
};
await expect(upsertAppPlaylist(db, playlist, capture)).resolves.toEqual(
{ success: true }
);
expect(phases).toEqual([
{
metadata: { itemCount: 2 },
phase: APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST,
},
{
metadata: { itemCount: 1 },
phase: APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE,
},
]);
expect(byteLength).not.toHaveBeenCalled();
expect(run).toHaveBeenCalledTimes(1);
});
});
@@ -1,6 +1,13 @@
import { eq, inArray } from 'drizzle-orm';
import * as schema from '@iptvnator/shared/database/schema';
import type { Channel, M3uFavoriteChannel } from '@iptvnator/shared/interfaces';
import {
APP_PLAYLIST_GET_PERFORMANCE_PHASE,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
type AppPlaylistGetPerformancePhase,
type AppPlaylistUpsertPerformancePhase,
type PerformancePhaseMetadata,
} from '@iptvnator/shared/interfaces';
import type { AppDatabase } from '../database.types';
import {
checkpointOperation,
@@ -19,6 +26,32 @@ const PLAYLIST_TYPES = {
const DEFAULT_BATCH_SIZE = 100;
export interface AppPlaylistUpsertPhaseCapture {
captureAsync: <TResult>(
phase: AppPlaylistUpsertPerformancePhase,
execute: () => Promise<TResult>,
metadata?: (result: TResult) => PerformancePhaseMetadata
) => Promise<TResult>;
captureSync: <TResult>(
phase: AppPlaylistUpsertPerformancePhase,
execute: () => TResult,
metadata?: (result: TResult) => PerformancePhaseMetadata
) => TResult;
}
export interface AppPlaylistGetPhaseCapture {
captureAsync: <TResult>(
phase: AppPlaylistGetPerformancePhase,
execute: () => Promise<TResult>,
metadata?: (result: TResult) => PerformancePhaseMetadata
) => Promise<TResult>;
captureSync: <TResult>(
phase: AppPlaylistGetPerformancePhase,
execute: () => TResult,
metadata?: (result: TResult) => PerformancePhaseMetadata
) => TResult;
}
type PlaylistType = (typeof PLAYLIST_TYPES)[keyof typeof PLAYLIST_TYPES];
const PLAYLIST_TYPE_VALUES = new Set<PlaylistType>(
Object.values(PLAYLIST_TYPES)
@@ -196,6 +229,21 @@ function buildPlaylistRow(
};
}
function getPlaylistItemCount(
playlist: Record<string, unknown>
): number | undefined {
try {
const value = playlist.playlist;
if (typeof value !== 'object' || value === null) {
return undefined;
}
const items = Reflect.get(value, 'items');
return Array.isArray(items) ? items.length : undefined;
} catch {
return undefined;
}
}
export function parseAppPlaylist(
row: schema.Playlist
): Record<string, unknown> {
@@ -300,17 +348,42 @@ export async function createPlaylist(
export async function upsertAppPlaylist(
db: AppDatabase,
playlist: Record<string, unknown>
playlist: Record<string, unknown>,
capturePhase?: AppPlaylistUpsertPhaseCapture
): Promise<{ success: boolean }> {
const row = buildPlaylistRow(playlist);
const row = capturePhase
? capturePhase.captureSync(
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST,
() => buildPlaylistRow(playlist),
() => ({
itemCount: getPlaylistItemCount(playlist),
})
)
: buildPlaylistRow(playlist);
if (!row) {
throw new Error('Playlist ID is required for upsert');
}
await db.insert(schema.playlists).values(row).onConflictDoUpdate({
target: schema.playlists.id,
set: row,
});
const write = async () => {
await db
.insert(schema.playlists)
.values(row)
.onConflictDoUpdate({
target: schema.playlists.id,
set: row,
});
};
if (capturePhase) {
await capturePhase.captureAsync(
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE,
write,
() => ({
itemCount: 1,
})
);
} else {
await write();
}
return { success: true };
}
@@ -392,14 +465,38 @@ export async function getAppPlaylistMetas(db: AppDatabase) {
);
}
export async function getAppPlaylist(db: AppDatabase, playlistId: string) {
const rows = await db
.select()
.from(schema.playlists)
.where(eq(schema.playlists.id, playlistId))
.limit(1);
export async function getAppPlaylist(
db: AppDatabase,
playlistId: string,
capturePhase?: AppPlaylistGetPhaseCapture
) {
const read = () =>
db
.select()
.from(schema.playlists)
.where(eq(schema.playlists.id, playlistId))
.limit(1);
const rows = capturePhase
? await capturePhase.captureAsync(
APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
read,
(value) => ({ itemCount: value.length })
)
: await read();
const deserialize = () =>
rows[0] ? parseAppPlaylist(rows[0]) : null;
return rows[0] ? parseAppPlaylist(rows[0]) : null;
return capturePhase
? capturePhase.captureSync(
APP_PLAYLIST_GET_PERFORMANCE_PHASE.DESERIALIZE_PLAYLIST,
deserialize,
(value) => ({
itemCount: value
? (getPlaylistItemCount(value) ?? 0)
: 0,
})
)
: deserialize();
}
export async function getAppPlaylistFavoriteChannels(
@@ -0,0 +1,215 @@
import {
M3U_IMPORT_PERFORMANCE_PHASE_EVENT_CHANNEL,
PERFORMANCE_PHASE_BOUNDARY,
type M3uImportPerformancePhase,
type PerformancePhaseEvent,
type PerformancePhaseMetadata,
} from '@iptvnator/shared/interfaces';
import { randomUUID } from 'node:crypto';
import { channel, type Channel } from 'node:diagnostics_channel';
import { performance } from 'node:perf_hooks';
export interface M3uImportPerformanceCaptureRuntime {
readonly createRequestId: () => string;
readonly readEpochMs: () => number;
readonly readMonotonicMs: () => number;
}
export interface M3uImportPerformanceCapture {
measure<TResult>(
phase: M3uImportPerformancePhase,
execute: () => TResult,
metadata?: (result: TResult) => PerformancePhaseMetadata
): TResult;
measureAsync<TResult>(
phase: M3uImportPerformancePhase,
execute: () => Promise<TResult>,
metadata?: (result: TResult) => PerformancePhaseMetadata
): Promise<TResult>;
}
interface CaptureOptions {
readonly emit?: (
event: PerformancePhaseEvent<M3uImportPerformancePhase>
) => void;
readonly enabled: boolean;
readonly requestId?: string;
readonly runtime?: M3uImportPerformanceCaptureRuntime;
}
interface PhaseStart {
readonly monotonicMs: number;
readonly phase: M3uImportPerformancePhase;
}
const DEFAULT_RUNTIME: M3uImportPerformanceCaptureRuntime = {
createRequestId: () => randomUUID(),
readEpochMs: () => performance.timeOrigin + performance.now(),
readMonotonicMs: () => performance.now(),
};
let performanceChannel: Channel | null = null;
let fallbackRequestSequence = 0;
export function publishM3uImportPerformancePhaseEvent(
event: PerformancePhaseEvent<M3uImportPerformancePhase>
): void {
try {
performanceChannel ??= channel(
M3U_IMPORT_PERFORMANCE_PHASE_EVENT_CHANNEL
);
performanceChannel.publish(event);
} catch {
// Development-only instrumentation must never affect playlist import.
}
}
export function createM3uImportPerformanceCapture(
options: CaptureOptions
): M3uImportPerformanceCapture | null {
if (!options.enabled) {
return null;
}
const runtime = options.runtime ?? DEFAULT_RUNTIME;
const requestId =
normalizeRequestId(options.requestId) ?? createRequestId(runtime);
const emit = options.emit ?? publishM3uImportPerformancePhaseEvent;
const begin = (phase: M3uImportPerformancePhase): PhaseStart => {
const monotonicMs = safeReadMonotonicMs(runtime);
safeEmit(emit, {
boundary: PERFORMANCE_PHASE_BOUNDARY.START,
durationMs: null,
epochMs: safeReadEpochMs(runtime),
metadata: undefined,
phase,
requestId,
});
return { monotonicMs, phase };
};
const end = (
start: PhaseStart,
createMetadata?: () => PerformancePhaseMetadata
): void => {
const endedMonotonicMs = safeReadMonotonicMs(runtime);
const durationMs = Math.max(0, endedMonotonicMs - start.monotonicMs);
const epochMs = safeReadEpochMs(runtime);
safeEmit(emit, {
boundary: PERFORMANCE_PHASE_BOUNDARY.END,
durationMs: Number.isFinite(durationMs) ? durationMs : 0,
epochMs,
metadata: sanitizeMetadata(safelyCreateMetadata(createMetadata)),
phase: start.phase,
requestId,
});
};
return {
measure: (phase, execute, metadata) => {
const start = begin(phase);
try {
const result = execute();
end(start, metadata ? () => metadata(result) : undefined);
return result;
} catch (error) {
end(start);
throw error;
}
},
measureAsync: async (phase, execute, metadata) => {
const start = begin(phase);
try {
const result = await execute();
end(start, metadata ? () => metadata(result) : undefined);
return result;
} catch (error) {
end(start);
throw error;
}
},
};
}
function createRequestId(runtime: M3uImportPerformanceCaptureRuntime): string {
try {
const requestId = normalizeRequestId(runtime.createRequestId());
if (requestId) {
return requestId;
}
} catch {
// Fall through to a process-local, credential-free identifier.
}
fallbackRequestSequence += 1;
return `m3u-import-${Date.now()}-${fallbackRequestSequence}`;
}
function normalizeRequestId(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null;
}
function safeReadEpochMs(runtime: M3uImportPerformanceCaptureRuntime): number {
try {
const value = runtime.readEpochMs();
return Number.isFinite(value) ? value : Date.now();
} catch {
return Date.now();
}
}
function safeReadMonotonicMs(
runtime: M3uImportPerformanceCaptureRuntime
): number {
try {
const value = runtime.readMonotonicMs();
return Number.isFinite(value) ? value : 0;
} catch {
return 0;
}
}
function safelyCreateMetadata(
createMetadata: (() => PerformancePhaseMetadata) | undefined
): PerformancePhaseMetadata | undefined {
if (!createMetadata) {
return undefined;
}
try {
return createMetadata();
} catch {
return undefined;
}
}
function sanitizeMetadata(
metadata: PerformancePhaseMetadata | undefined
): PerformancePhaseMetadata | undefined {
if (!metadata) {
return undefined;
}
const sanitized: PerformancePhaseMetadata = {};
if (isCount(metadata.byteCount)) {
(sanitized as { byteCount?: number }).byteCount = metadata.byteCount;
}
if (isCount(metadata.itemCount)) {
(sanitized as { itemCount?: number }).itemCount = metadata.itemCount;
}
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}
function isCount(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
function safeEmit(
emit: (event: PerformancePhaseEvent<M3uImportPerformancePhase>) => void,
event: PerformancePhaseEvent<M3uImportPerformancePhase>
): void {
try {
emit(event);
} catch {
// Instrumentation delivery is deliberately fail-neutral.
}
}
@@ -0,0 +1,312 @@
import {
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
M3U_IMPORT_PERFORMANCE_PHASE,
type PerformancePhaseEvent,
} from '@iptvnator/shared/interfaces';
import {
createM3uImportPerformanceCapture,
type M3uImportPerformanceCaptureRuntime,
} from './playlist-import-performance';
import { fetchPlaylistFromUrl } from './playlist-source';
const mockCreatePlaylistObject = jest.fn();
const mockGetFilenameFromUrl = jest.fn();
const mockParse = jest.fn();
const mockRequestWithValidatedRedirects = jest.fn();
jest.mock('@iptvnator/shared/m3u-utils', () => ({
createPlaylistObject: (...args: unknown[]) =>
mockCreatePlaylistObject(...args),
getFilenameFromUrl: (...args: unknown[]) => mockGetFilenameFromUrl(...args),
}));
jest.mock('iptv-playlist-parser', () => ({
parse: (...args: unknown[]) => mockParse(...args),
}));
jest.mock('../util/secure-https', () => ({
createPlaylistAgentFactory: jest.fn(() => jest.fn()),
}));
jest.mock('../util/validated-axios', () => ({
requestWithValidatedRedirects: (...args: unknown[]) =>
mockRequestWithValidatedRedirects(...args),
}));
type ImportPhaseEvent = PerformancePhaseEvent<
(typeof M3U_IMPORT_PERFORMANCE_PHASE)[keyof typeof M3U_IMPORT_PERFORMANCE_PHASE]
>;
function createRuntime(): M3uImportPerformanceCaptureRuntime {
const epochValues = [100, 110, 120, 130, 140, 150];
const monotonicValues = [10, 14, 20, 23, 30, 36];
return {
createRequestId: jest.fn(() => 'm3u-request-1'),
readEpochMs: jest.fn(() => epochValues.shift() ?? 150),
readMonotonicMs: jest.fn(() => monotonicValues.shift() ?? 36),
};
}
describe('URL playlist import performance phases', () => {
afterEach(() => {
jest.restoreAllMocks();
});
beforeEach(() => {
mockCreatePlaylistObject.mockReset();
mockGetFilenameFromUrl.mockReset();
mockParse.mockReset();
mockRequestWithValidatedRedirects.mockReset();
});
it('does not allocate phase state or read clocks while capture is disabled', async () => {
const runtime = {
createRequestId: jest.fn(() => {
throw new Error('request id must stay untouched');
}),
readEpochMs: jest.fn(() => {
throw new Error('epoch clock must stay untouched');
}),
readMonotonicMs: jest.fn(() => {
throw new Error('monotonic clock must stay untouched');
}),
};
const emit = jest.fn();
const playlist = { playlist: { items: [] }, title: 'Synthetic' };
mockRequestWithValidatedRedirects.mockResolvedValue({
data: '#EXTM3U',
});
mockParse.mockReturnValue({ items: [] });
mockGetFilenameFromUrl.mockReturnValue('synthetic.m3u');
mockCreatePlaylistObject.mockReturnValue(playlist);
const capture = createM3uImportPerformanceCapture({
emit,
enabled: false,
runtime,
});
const result = await fetchPlaylistFromUrl(
'http://127.0.0.1:43210/synthetic-performance.m3u',
undefined,
{ performanceCapture: capture }
);
expect(capture).toBeNull();
expect(result).toBe(playlist);
expect(emit).not.toHaveBeenCalled();
expect(runtime.createRequestId).not.toHaveBeenCalled();
expect(runtime.readEpochMs).not.toHaveBeenCalled();
expect(runtime.readMonotonicMs).not.toHaveBeenCalled();
});
it('emits ordered epoch-aligned pairs with durations and only byte/item metadata', async () => {
const events: ImportPhaseEvent[] = [];
const body = '#EXTM3U\n#EXTINF:-1,One\nhttps://stream.test/1';
const bodyBytes = Buffer.byteLength(body, 'utf8');
const parsed = { items: [{ name: 'One' }, { name: 'Two' }] };
const playlist = {
playlist: { items: parsed.items },
title: 'Synthetic',
};
mockRequestWithValidatedRedirects.mockResolvedValue({
data: body,
headers: {
'content-length': String(bodyBytes),
},
});
mockParse.mockReturnValue(parsed);
mockGetFilenameFromUrl.mockReturnValue('synthetic.m3u');
mockCreatePlaylistObject.mockReturnValue(playlist);
const capture = createM3uImportPerformanceCapture({
emit: (event) => events.push(event),
enabled: true,
requestId: 'm3u-request-1',
runtime: createRuntime(),
});
const byteLength = jest.spyOn(Buffer, 'byteLength');
const result = await fetchPlaylistFromUrl(
'http://127.0.0.1:43210/synthetic-performance.m3u',
undefined,
{ performanceCapture: capture }
);
expect(result).toBe(playlist);
expect(byteLength).not.toHaveBeenCalled();
expect(
events.map(
({
boundary,
durationMs,
epochMs,
metadata,
phase,
requestId,
}) => ({
boundary,
durationMs,
epochMs,
metadata,
phase,
requestId,
})
)
).toEqual([
{
boundary: 'start',
durationMs: null,
epochMs: 100,
metadata: undefined,
phase: M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE,
requestId: 'm3u-request-1',
},
{
boundary: 'end',
durationMs: 4,
epochMs: 110,
metadata: { byteCount: bodyBytes },
phase: M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE,
requestId: 'm3u-request-1',
},
{
boundary: 'start',
durationMs: null,
epochMs: 120,
metadata: undefined,
phase: M3U_IMPORT_PERFORMANCE_PHASE.PARSE_M3U,
requestId: 'm3u-request-1',
},
{
boundary: 'end',
durationMs: 3,
epochMs: 130,
metadata: { itemCount: 2 },
phase: M3U_IMPORT_PERFORMANCE_PHASE.PARSE_M3U,
requestId: 'm3u-request-1',
},
{
boundary: 'start',
durationMs: null,
epochMs: 140,
metadata: undefined,
phase: M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE,
requestId: 'm3u-request-1',
},
{
boundary: 'end',
durationMs: 6,
epochMs: 150,
metadata: { itemCount: 2 },
phase: M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE,
requestId: 'm3u-request-1',
},
]);
const serialized = JSON.stringify(events);
expect(serialized).not.toContain('127.0.0.1');
expect(serialized).not.toContain('stream.test');
expect(serialized).not.toContain('Synthetic');
expect(serialized).not.toContain('username');
expect(serialized).not.toContain('password');
expect(serialized).not.toContain(
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE
);
});
it('preserves exact results and errors when event or metadata hooks fail', async () => {
const resultIdentity = { result: 'same-object' };
const capture = createM3uImportPerformanceCapture({
emit: () => {
throw new Error('event sink failed');
},
enabled: true,
requestId: 'm3u-request-2',
runtime: createRuntime(),
});
expect(
capture?.measure(
M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE,
() => resultIdentity,
() => {
throw new Error('metadata failed');
}
)
).toBe(resultIdentity);
const businessError = new Error('download failed');
const events: ImportPhaseEvent[] = [];
mockRequestWithValidatedRedirects.mockRejectedValue(businessError);
const errorCapture = createM3uImportPerformanceCapture({
emit: (event) => events.push(event),
enabled: true,
requestId: 'm3u-request-3',
runtime: createRuntime(),
});
await expect(
fetchPlaylistFromUrl(
'http://127.0.0.1:43210/failure.m3u',
undefined,
{
performanceCapture: errorCapture,
}
)
).rejects.toBe(businessError);
expect(
events.map(({ boundary, phase }) => ({ boundary, phase }))
).toEqual([
{
boundary: 'start',
phase: M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE,
},
{
boundary: 'end',
phase: M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE,
},
]);
});
it('samples the end boundary before creating phase metadata', () => {
const events: ImportPhaseEvent[] = [];
let epochMs = 100;
let monotonicMs = 10;
const resultIdentity = { itemCount: 2 };
const capture = createM3uImportPerformanceCapture({
emit: (event) => events.push(event),
enabled: true,
requestId: 'm3u-request-boundary',
runtime: {
createRequestId: jest.fn(() => 'unused-request-id'),
readEpochMs: jest.fn(() => epochMs),
readMonotonicMs: jest.fn(() => monotonicMs),
},
});
const result = capture?.measure(
M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE,
() => {
epochMs = 110;
monotonicMs = 20;
return resultIdentity;
},
({ itemCount }) => {
epochMs = 1_110;
monotonicMs = 1_020;
return { itemCount };
}
);
expect(result).toBe(resultIdentity);
expect(events).toHaveLength(2);
expect(events[1]).toMatchObject({
boundary: 'end',
durationMs: 10,
epochMs: 110,
metadata: { itemCount: 2 },
});
});
});
@@ -13,8 +13,11 @@ import {
isInvalidTlsCertificateError,
} from '../util/security-errors';
import { requestWithValidatedRedirects } from '../util/validated-axios';
import type { M3uImportPerformanceCapture } from './playlist-import-performance';
import { M3U_IMPORT_PERFORMANCE_PHASE } from '@iptvnator/shared/interfaces';
export interface PlaylistFetchOptions {
performanceCapture?: M3uImportPerformanceCapture | null;
trustedInsecureTlsHosts?: readonly string[];
}
@@ -26,14 +29,67 @@ export interface PlaylistFetchOptions {
*/
export const PLAYLIST_FETCH_TIMEOUT_MS = 30000;
function countPlaylistItems(value: unknown): number | undefined {
try {
if (typeof value !== 'object' || value === null) {
return undefined;
}
const items = Reflect.get(value, 'items');
return Array.isArray(items) ? items.length : undefined;
} catch {
return undefined;
}
}
function countNormalizedPlaylistItems(value: unknown): number | undefined {
try {
if (typeof value !== 'object' || value === null) {
return undefined;
}
return countPlaylistItems(Reflect.get(value, 'playlist'));
} catch {
return undefined;
}
}
function readResponseContentLength(response: unknown): number | undefined {
try {
if (typeof response !== 'object' || response === null) {
return undefined;
}
const headers = Reflect.get(response, 'headers');
if (typeof headers !== 'object' || headers === null) {
return undefined;
}
const directValue = Reflect.get(headers, 'content-length');
const getHeader = Reflect.get(headers, 'get');
const value =
directValue ??
(typeof getHeader === 'function'
? Reflect.apply(getHeader, headers, ['content-length'])
: undefined);
const numericValue =
typeof value === 'number'
? value
: typeof value === 'string' && value.trim().length > 0
? Number(value)
: Number.NaN;
return Number.isSafeInteger(numericValue) && numericValue >= 0
? numericValue
: undefined;
} catch {
return undefined;
}
}
export async function fetchPlaylistFromUrl(
url: string,
title?: string,
options: PlaylistFetchOptions = {}
): Promise<Playlist> {
let result;
try {
result = await requestWithValidatedRedirects<string>(
const performanceCapture = options.performanceCapture;
const request = () =>
requestWithValidatedRedirects<string>(
url,
{
agentFactory: createPlaylistAgentFactory({
@@ -44,6 +100,17 @@ export async function fetchPlaylistFromUrl(
},
{ allowPrivateNetworks: true }
);
let result;
try {
result = performanceCapture
? await performanceCapture.measureAsync(
M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE,
request,
(response) => ({
byteCount: readResponseContentLength(response),
})
)
: await request();
} catch (error) {
if (isInvalidTlsCertificateError(error)) {
throw createInvalidTlsCertificateError(
@@ -53,19 +120,38 @@ export async function fetchPlaylistFromUrl(
throw error;
}
const parsedPlaylist = parse(result.data);
const extractedName = url && url.length > 1 ? getFilenameFromUrl(url) : '';
const playlistName =
!extractedName || extractedName === 'Untitled playlist'
? 'Imported from URL'
: extractedName;
const parsedPlaylist = performanceCapture
? performanceCapture.measure(
M3U_IMPORT_PERFORMANCE_PHASE.PARSE_M3U,
() => parse(result.data),
(parsed) => ({ itemCount: countPlaylistItems(parsed) })
)
: parse(result.data);
const normalize = (): Playlist => {
const extractedName =
url && url.length > 1 ? getFilenameFromUrl(url) : '';
const playlistName =
!extractedName || extractedName === 'Untitled playlist'
? 'Imported from URL'
: extractedName;
return createPlaylistObject(
title ?? playlistName,
parsedPlaylist,
url,
'URL'
);
return createPlaylistObject(
title ?? playlistName,
parsedPlaylist,
url,
'URL'
);
};
return performanceCapture
? performanceCapture.measure(
M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE,
normalize,
(playlist) => ({
itemCount: countNormalizedPlaylistItems(playlist),
})
)
: normalize();
}
export async function fetchPlaylistFromFile(
@@ -6,10 +6,13 @@ import type {
} from '@iptvnator/shared/interfaces';
import {
AUTO_UPDATE_PLAYLISTS,
M3U_IMPORT_PERFORMANCE_PHASE,
M3U_IMPORT_PERFORMANCE_PHASE_EVENT_CHANNEL,
PLAYLIST_CANCEL_REFRESH,
PLAYLIST_REFRESH,
PLAYLIST_REFRESH_EVENT,
} from '@iptvnator/shared/interfaces';
import { channel } from 'node:diagnostics_channel';
import { resolve } from 'node:path';
type IpcHandler = (event: MockIpcEvent, ...args: unknown[]) => Promise<unknown>;
@@ -41,6 +44,8 @@ const mockCreatePlaylistObject = jest.fn();
const mockGetFilenameFromUrl = jest.fn();
const mockResolveWorkerRuntimeBootstrap = jest.fn();
const mockWorkerInstances: MockWorker[] = [];
const PERF_CAPTURE_ENV = 'IPTVNATOR_PERF_CAPTURE';
const originalPerformanceCaptureValue = process.env[PERF_CAPTURE_ENV];
jest.mock('electron', () => ({
app: {
@@ -169,6 +174,11 @@ describe('playlist IPC events', () => {
});
afterEach(() => {
if (originalPerformanceCaptureValue === undefined) {
delete process.env[PERF_CAPTURE_ENV];
} else {
process.env[PERF_CAPTURE_ENV] = originalPerformanceCaptureValue;
}
consoleErrorSpy.mockRestore();
consoleLogSpy.mockRestore();
consoleWarnSpy.mockRestore();
@@ -210,6 +220,101 @@ describe('playlist IPC events', () => {
expect(result).toEqual(playlist);
});
it('publishes request-scoped URL import phases only through the opt-in internal channel', async () => {
process.env[PERF_CAPTURE_ENV] = '1';
const body = '#EXTM3U\n#EXTINF:-1,News\nhttps://stream.test/news';
const parsedPlaylist = { items: [{ name: 'News' }] };
const playlist = createPlaylist({
title: 'synthetic-performance.m3u',
url: 'http://127.0.0.1:43210/synthetic-performance.m3u',
});
const events: unknown[] = [];
const performanceChannel = channel(
M3U_IMPORT_PERFORMANCE_PHASE_EVENT_CHANNEL
);
const subscriber = (event: unknown) => events.push(event);
mockAxiosGet.mockResolvedValue({
data: body,
headers: {
'content-length': String(Buffer.byteLength(body, 'utf8')),
},
});
mockParse.mockReturnValue(parsedPlaylist);
mockGetFilenameFromUrl.mockReturnValue('synthetic-performance.m3u');
mockCreatePlaylistObject.mockReturnValue(playlist);
performanceChannel.subscribe(subscriber);
try {
await expect(
getHandler('fetch-playlist-by-url')(
createIpcEvent(),
'http://127.0.0.1:43210/synthetic-performance.m3u'
)
).resolves.toBe(playlist);
} finally {
performanceChannel.unsubscribe(subscriber);
}
expect(
events.map((event) => {
const marker = event as {
boundary: string;
metadata?: { byteCount?: number; itemCount?: number };
phase: string;
requestId: string;
};
return {
boundary: marker.boundary,
metadata: marker.metadata,
phase: marker.phase,
requestIdType: typeof marker.requestId,
};
})
).toEqual([
{
boundary: 'start',
metadata: undefined,
phase: M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE,
requestIdType: 'string',
},
{
boundary: 'end',
metadata: { byteCount: Buffer.byteLength(body, 'utf8') },
phase: M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE,
requestIdType: 'string',
},
{
boundary: 'start',
metadata: undefined,
phase: M3U_IMPORT_PERFORMANCE_PHASE.PARSE_M3U,
requestIdType: 'string',
},
{
boundary: 'end',
metadata: { itemCount: 1 },
phase: M3U_IMPORT_PERFORMANCE_PHASE.PARSE_M3U,
requestIdType: 'string',
},
{
boundary: 'start',
metadata: undefined,
phase: M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE,
requestIdType: 'string',
},
{
boundary: 'end',
metadata: { itemCount: 1 },
phase: M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE,
requestIdType: 'string',
},
]);
expect(
new Set(events.map((event) => (event as any).requestId)).size
).toBe(1);
expect(JSON.stringify(events)).not.toContain('stream.test');
});
it('returns null when the open playlist dialog is cancelled', async () => {
mockShowOpenDialog.mockResolvedValue({
canceled: true,
@@ -32,6 +32,8 @@ import {
fetchPlaylistFromUrl,
} from './playlist-source';
import { PlaylistWriteAuthorizer } from './playlist-write-authorization';
import { createM3uImportPerformanceCapture } from './playlist-import-performance';
import { isPerformanceCaptureEnabled } from '../services/debug-trace';
export default class PlaylistEvents {
static bootstrapPlaylistEvents(): Electron.IpcMain {
@@ -95,7 +97,11 @@ ipcMain.handle(
options?: ElectronBridgeTrustOptions
) => {
try {
const performanceCapture = createM3uImportPerformanceCapture({
enabled: isPerformanceCaptureEnabled(),
});
return await fetchPlaylistFromUrl(url, title, {
performanceCapture,
trustedInsecureTlsHosts: options?.trustedInsecureTlsHosts,
});
} catch (error) {
@@ -64,6 +64,8 @@ import {
getAppState,
getPlaylist,
setAppState,
type AppPlaylistGetPhaseCapture,
type AppPlaylistUpsertPhaseCapture,
updatePlaylist,
upsertAppPlaylist,
upsertAppPlaylists,
@@ -93,9 +95,14 @@ import {
executeWithWorkerPerformanceCapture,
registerDatabaseWorkerPerformanceCapture,
releaseDatabaseWorkerPerformanceCapture,
stampWorkerPerformanceResponsePostedEpoch,
startWorkerPerformanceCapture,
type WorkerPerformanceCapture,
} from './worker-performance-capture';
import {
captureWorkerPerformancePhase,
captureWorkerPerformancePhaseAsync,
} from './worker-performance-phase';
import {
handleDatabaseWorkerPostGcHeapRequest,
isDatabaseWorkerPostGcHeapRequest,
@@ -349,7 +356,8 @@ function releasePreRegisteredOperation(
async function executeRequest(
message: DbWorkerRequestMessage,
preRegisteredState?: ActiveOperationState
preRegisteredState?: ActiveOperationState,
performanceCapture: WorkerPerformanceCapture | null = null
) {
const db = await getWorkerDatabase();
@@ -430,11 +438,7 @@ async function executeRequest(
kind?: 'all' | 'vod' | 'series';
limit?: number;
playlistType?:
| 'xtream'
| 'stalker'
| 'm3u-file'
| 'm3u-text'
| 'm3u-url';
'xtream' | 'stalker' | 'm3u-file' | 'm3u-text' | 'm3u-url';
};
return getGlobalRecentlyAdded(
db,
@@ -575,9 +579,29 @@ async function executeRequest(
}
case 'DB_UPSERT_APP_PLAYLIST': {
const capturePhase: AppPlaylistUpsertPhaseCapture | undefined =
performanceCapture
? {
captureAsync: (phase, execute, metadata) =>
captureWorkerPerformancePhaseAsync(
performanceCapture,
phase,
execute,
metadata
),
captureSync: (phase, execute, metadata) =>
captureWorkerPerformancePhase(
performanceCapture,
phase,
execute,
metadata
),
}
: undefined;
return upsertAppPlaylist(
db,
message.payload as Record<string, unknown>
message.payload as Record<string, unknown>,
capturePhase
);
}
@@ -596,7 +620,26 @@ async function executeRequest(
case 'DB_GET_APP_PLAYLIST': {
const payload = message.payload as { playlistId: string };
return getAppPlaylist(db, payload.playlistId);
const capturePhase: AppPlaylistGetPhaseCapture | undefined =
performanceCapture
? {
captureAsync: (phase, execute, metadata) =>
captureWorkerPerformancePhaseAsync(
performanceCapture,
phase,
execute,
metadata
),
captureSync: (phase, execute, metadata) =>
captureWorkerPerformancePhase(
performanceCapture,
phase,
execute,
metadata
),
}
: undefined;
return getAppPlaylist(db, payload.playlistId, capturePhase);
}
case 'DB_GET_APP_PLAYLIST_FAVORITE_CHANNELS': {
@@ -1013,7 +1056,9 @@ parentPort.on('message', async (message: DbWorkerIncomingMessage) => {
let performanceCapture: WorkerPerformanceCapture | null = null;
const execution = await (async () => {
try {
performanceCapture = startWorkerPerformanceCapture();
performanceCapture = startWorkerPerformanceCapture({
requestId: message.requestId,
});
registerDatabaseWorkerPerformanceCapture(
activePerformanceCaptures,
performanceCapture
@@ -1021,7 +1066,12 @@ parentPort.on('message', async (message: DbWorkerIncomingMessage) => {
await armWorkerPerformanceCapture(performanceCapture);
return await executeWithWorkerPerformanceCapture(
performanceCapture,
() => executeRequest(message, preRegisteredOperation?.state)
() =>
executeRequest(
message,
preRegisteredOperation?.state,
performanceCapture
)
);
} finally {
releaseDatabaseWorkerPerformanceCapture(
@@ -1038,7 +1088,10 @@ parentPort.on('message', async (message: DbWorkerIncomingMessage) => {
requestId: message.requestId,
success: true,
result: execution.result,
performance: execution.performance,
performance: stampWorkerPerformanceResponsePostedEpoch(
performanceCapture,
execution.performance
),
});
} else {
console.error(
@@ -1051,7 +1104,10 @@ parentPort.on('message', async (message: DbWorkerIncomingMessage) => {
requestId: message.requestId,
success: false,
error: serializeError(execution.error),
performance: execution.performance,
performance: stampWorkerPerformanceResponsePostedEpoch(
performanceCapture,
execution.performance
),
});
}
});
@@ -25,6 +25,7 @@ import { PLAYLIST_FETCH_TIMEOUT_MS } from '../events/playlist-source';
import {
armWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
stampWorkerPerformanceResponsePostedEpoch,
startWorkerPerformanceCapture,
} from './worker-performance-capture';
@@ -229,7 +230,10 @@ parentPort.on(
type: 'response',
success: true,
result: execution.result,
performance: execution.performance,
performance: stampWorkerPerformanceResponsePostedEpoch(
performanceCapture,
execution.performance
),
});
} else {
const error = execution.error;
@@ -237,7 +241,10 @@ parentPort.on(
type: 'response',
success: false,
error: serializeError(error),
performance: execution.performance,
performance: stampWorkerPerformanceResponsePostedEpoch(
performanceCapture,
execution.performance
),
});
}
} finally {
@@ -82,6 +82,9 @@ function mockPerformanceCapture(
),
registerDatabaseWorkerPerformanceCapture: jest.fn(),
releaseDatabaseWorkerPerformanceCapture: jest.fn(),
stampWorkerPerformanceResponsePostedEpoch: jest.fn(
(_capture: unknown, performance: unknown) => performance
),
startWorkerPerformanceCapture: jest.fn(() => ({})),
}));
}
@@ -177,6 +177,7 @@ function createFallbackResult(
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_UTILIZATION_UNAVAILABLE,
histogramFlushedEpochMs: null,
invalidReason,
...copyPhaseEvents(capture),
requestReceivedEpochMs: capture.requestReceivedEpochMs,
threadCpuSystemMicros: null,
threadCpuUnavailableReason:
@@ -213,6 +214,7 @@ export function createWorkerPerformanceCaptureResult(
capture.eventLoopUtilizationUnavailableReason,
histogramFlushedEpochMs: capture.histogramFlushedEpochMs,
invalidReason: capture.invalidReason,
...copyPhaseEvents(capture),
requestReceivedEpochMs: capture.requestReceivedEpochMs,
...threadCpu,
threadCpuUnavailableReason: capture.threadCpuUnavailableReason,
@@ -226,6 +228,18 @@ export function createWorkerPerformanceCaptureResult(
}
}
function copyPhaseEvents(
capture: WorkerPerformanceCapture
): Pick<WorkerPerformanceCaptureResult, 'phaseEvents'> {
try {
return capture.phaseEvents.length > 0
? { phaseEvents: Object.freeze([...capture.phaseEvents]) }
: {};
} catch {
return {};
}
}
export function finishWorkerPerformanceMetricBoundaries(
capture: WorkerPerformanceCapture
): void {
@@ -1,4 +1,8 @@
import type { EventLoopUtilization } from 'node:perf_hooks';
import type {
AppPlaylistPerformancePhase,
PerformancePhaseEvent,
} from '@iptvnator/shared/interfaces';
export const WORKER_PERFORMANCE_INVALID_REASON = {
OVERLAPPING_DATABASE_WORKER_REQUESTS:
@@ -46,7 +50,9 @@ export interface WorkerPerformanceCaptureResult {
eventLoopUtilizationUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
histogramFlushedEpochMs: number | null;
invalidReason: WorkerPerformanceInvalidReason | null;
phaseEvents?: readonly PerformancePhaseEvent<AppPlaylistPerformancePhase>[];
requestReceivedEpochMs: number;
responsePostedEpochMs?: number;
threadCpuSystemMicros: number | null;
threadCpuUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
threadCpuUserMicros: number | null;
@@ -65,6 +71,7 @@ export interface WorkerPerformanceCaptureRuntime {
export interface WorkerPerformanceCaptureOptions {
enabled?: boolean;
requestId?: string;
runtime?: WorkerPerformanceCaptureRuntime;
}
@@ -84,7 +91,9 @@ export interface WorkerPerformanceCapture {
histogramDisabled: boolean;
histogramFlushedEpochMs: number | null;
invalidReason: WorkerPerformanceInvalidReason | null;
phaseEvents: PerformancePhaseEvent<AppPlaylistPerformancePhase>[];
requestReceivedEpochMs: number;
requestId: string | null;
runtime: WorkerPerformanceCaptureRuntime;
threadCpuEnd: NodeJS.CpuUsage | null;
threadCpuStart: NodeJS.CpuUsage | null;
@@ -2,6 +2,7 @@ import {
WORKER_PERFORMANCE_UNAVAILABLE_REASON,
armWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
stampWorkerPerformanceResponsePostedEpoch,
startWorkerPerformanceCapture,
} from './worker-performance-capture';
import { createFakeRuntime } from './worker-performance-capture.test-harness';
@@ -86,6 +87,34 @@ describe('worker performance capture', () => {
expect('reset' in harness.histogram).toBe(false);
});
it('stamps the worker-send boundary only after request profiling has finished', async () => {
const harness = createFakeRuntime();
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'result'
);
const stamped = stampWorkerPerformanceResponsePostedEpoch(
capture,
execution.performance
);
expect(stamped).toMatchObject({
histogramFlushedEpochMs: 145,
responsePostedEpochMs: 149,
workEndedEpochMs: 140,
});
expect(harness.lifecycle.at(-1)).toBe('epoch:149');
expect(
stampWorkerPerformanceResponsePostedEpoch(null, undefined)
).toBeUndefined();
});
it('finishes profiling before returning a rejected execution to worker side effects', async () => {
const harness = createFakeRuntime();
const capture = startWorkerPerformanceCapture({
@@ -57,7 +57,13 @@ export function startWorkerPerformanceCapture(
histogramDisabled: false,
histogramFlushedEpochMs: null,
invalidReason: null,
phaseEvents: [],
requestReceivedEpochMs: safeReadEpochMs(runtime),
requestId:
typeof options.requestId === 'string' &&
options.requestId.length > 0
? options.requestId
: null,
runtime,
threadCpuEnd: null,
threadCpuStart: null,
@@ -269,3 +275,21 @@ export function releaseDatabaseWorkerPerformanceCapture(
activeCaptures.delete(capture);
}
}
export function stampWorkerPerformanceResponsePostedEpoch(
capture: WorkerPerformanceCapture | null,
performance: WorkerPerformanceCaptureResult | undefined
): WorkerPerformanceCaptureResult | undefined {
if (!capture || !performance) {
return performance;
}
try {
return {
...performance,
responsePostedEpochMs: safeReadEpochMs(capture.runtime),
};
} catch {
return performance;
}
}
@@ -33,6 +33,12 @@ describe('worker performance integration order', () => {
expect(
handler.indexOf('executeWithWorkerPerformanceCapture')
).toBeLessThan(handler.indexOf('postMessage'));
expect(
handler.match(/stampWorkerPerformanceResponsePostedEpoch/g)
).toHaveLength(2);
expect(handler).toMatch(
/postMessage\(\{[\s\S]*?performance:\s*stampWorkerPerformanceResponsePostedEpoch\([\s\S]*?execution\.performance[\s\S]*?\),[\s\S]*?\}\)/
);
expect(handler).not.toContain('await previous');
expect(handler).not.toContain('requestQueue');
});
@@ -52,6 +58,34 @@ describe('worker performance integration order', () => {
expect(executionIndex).toBeLessThan(
handler.indexOf('postMessage', executionIndex)
);
expect(
handler.match(/stampWorkerPerformanceResponsePostedEpoch/g)
).toHaveLength(2);
});
it('passes the request capture into the single-playlist upsert boundary', () => {
const source = readWorkerSource('database.worker.ts');
const upsertCase = source.slice(
source.indexOf("case 'DB_UPSERT_APP_PLAYLIST':"),
source.indexOf("case 'DB_UPSERT_APP_PLAYLISTS':")
);
expect(upsertCase).toContain('captureWorkerPerformancePhase');
expect(upsertCase).toContain('performanceCapture');
expect(upsertCase).toContain('upsertAppPlaylist');
});
it('passes async read and sync deserialize capture into the single-playlist GET boundary', () => {
const source = readWorkerSource('database.worker.ts');
const getCase = source.slice(
source.indexOf("case 'DB_GET_APP_PLAYLIST':"),
source.indexOf("case 'DB_GET_APP_PLAYLIST_FAVORITE_CHANNELS':")
);
expect(getCase).toContain('captureWorkerPerformancePhaseAsync');
expect(getCase).toContain('captureWorkerPerformancePhase');
expect(getCase).toContain('performanceCapture');
expect(getCase).toContain('getAppPlaylist');
});
it('preserves success and business errors when the finish seam throws once', async () => {
@@ -0,0 +1,245 @@
import {
APP_PLAYLIST_GET_PERFORMANCE_PHASE,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
type PerformancePhaseMetadata,
} from '@iptvnator/shared/interfaces';
import {
finishWorkerPerformanceCapture,
startWorkerPerformanceCapture,
} from './worker-performance-capture';
import {
captureWorkerPerformancePhase,
captureWorkerPerformancePhaseAsync,
} from './worker-performance-phase';
import { createFakeRuntime } from './worker-performance-capture.test-harness';
describe('worker request performance phases', () => {
it('executes directly without phase allocation or metadata work when disabled', () => {
const resultIdentity = { success: true };
const execute = jest.fn(() => resultIdentity);
const metadata = jest.fn(() => {
throw new Error('metadata must remain untouched');
});
const result = captureWorkerPerformancePhase(
null,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST,
execute,
metadata
);
expect(result).toBe(resultIdentity);
expect(execute).toHaveBeenCalledTimes(1);
expect(metadata).not.toHaveBeenCalled();
});
it('adds ordered request-scoped pairs to the existing performance envelope', async () => {
const harness = createFakeRuntime();
const monotonicValues = [10, 17, 20, 31];
const capture = startWorkerPerformanceCapture({
enabled: true,
requestId: 'db-request-1',
runtime: {
...harness.runtime,
readMonotonicMs: () => monotonicValues.shift() ?? 31,
},
});
const rowIdentity = { payload: '{"playlist":"synthetic"}' };
expect(
captureWorkerPerformancePhase(
capture,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST,
() => rowIdentity,
() =>
({
byteCount: 24,
itemCount: 100_000,
secret: 'must-not-be-copied',
}) as PerformancePhaseMetadata
)
).toBe(rowIdentity);
captureWorkerPerformancePhase(
capture,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE,
() => undefined,
() => ({ byteCount: 24, itemCount: 1 })
);
const result = await finishWorkerPerformanceCapture(capture);
expect(result?.phaseEvents).toEqual([
{
boundary: 'start',
durationMs: null,
epochMs: 110,
metadata: undefined,
phase: APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST,
requestId: 'db-request-1',
},
{
boundary: 'end',
durationMs: 7,
epochMs: 140,
metadata: { byteCount: 24, itemCount: 100_000 },
phase: APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST,
requestId: 'db-request-1',
},
{
boundary: 'start',
durationMs: null,
epochMs: 145,
metadata: undefined,
phase: APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE,
requestId: 'db-request-1',
},
{
boundary: 'end',
durationMs: 11,
epochMs: 149,
metadata: { byteCount: 24, itemCount: 1 },
phase: APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE,
requestId: 'db-request-1',
},
]);
expect(JSON.stringify(result?.phaseEvents)).not.toContain('secret');
});
it('preserves exact error identity and still closes the phase pair', async () => {
const harness = createFakeRuntime();
const capture = startWorkerPerformanceCapture({
enabled: true,
requestId: 'db-request-error',
runtime: harness.runtime,
});
const businessError = new Error('sqlite write failed');
expect(() =>
captureWorkerPerformancePhase(
capture,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE,
() => {
throw businessError;
},
() => {
throw new Error('metadata failed');
}
)
).toThrow(businessError);
const result = await finishWorkerPerformanceCapture(capture);
expect(
result?.phaseEvents?.map(({ boundary, metadata }) => ({
boundary,
metadata,
}))
).toEqual([
{ boundary: 'start', metadata: undefined },
{ boundary: 'end', metadata: undefined },
]);
});
it('returns the exact business promise without metadata work when async capture is disabled', async () => {
const resultIdentity = { rows: 1 };
const businessPromise = Promise.resolve(resultIdentity);
const execute = jest.fn(() => businessPromise);
const metadata = jest.fn(() => {
throw new Error('metadata must remain untouched');
});
const result = captureWorkerPerformancePhaseAsync(
null,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
execute,
metadata
);
expect(result).toBe(businessPromise);
await expect(result).resolves.toBe(resultIdentity);
expect(execute).toHaveBeenCalledTimes(1);
expect(metadata).not.toHaveBeenCalled();
});
it('closes an async phase only after the awaited operation settles', async () => {
const harness = createFakeRuntime();
const monotonicValues = [10, 27];
const capture = startWorkerPerformanceCapture({
enabled: true,
requestId: 'db-get-request',
runtime: {
...harness.runtime,
readMonotonicMs: () => monotonicValues.shift() ?? 27,
},
});
const rows = [{ id: 'synthetic-playlist' }];
let resolveRows!: (value: typeof rows) => void;
const pendingRows = new Promise<typeof rows>((resolve) => {
resolveRows = resolve;
});
const result = captureWorkerPerformancePhaseAsync(
capture,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
() => pendingRows,
(value) => ({ itemCount: value.length })
);
expect(capture?.phaseEvents).toHaveLength(1);
resolveRows(rows);
await expect(result).resolves.toBe(rows);
expect(capture?.phaseEvents).toHaveLength(2);
const performance = await finishWorkerPerformanceCapture(capture);
expect(performance?.phaseEvents).toEqual([
{
boundary: 'start',
durationMs: null,
epochMs: 110,
metadata: undefined,
phase: APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
requestId: 'db-get-request',
},
{
boundary: 'end',
durationMs: 17,
epochMs: 140,
metadata: { itemCount: 1 },
phase: APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
requestId: 'db-get-request',
},
]);
});
it('preserves async rejection identity and closes the phase without metadata', async () => {
const harness = createFakeRuntime();
const capture = startWorkerPerformanceCapture({
enabled: true,
requestId: 'db-get-error',
runtime: harness.runtime,
});
const businessError = new Error('sqlite read failed');
await expect(
captureWorkerPerformancePhaseAsync(
capture,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ,
() => Promise.reject(businessError),
() => {
throw new Error('metadata must not run for errors');
}
)
).rejects.toBe(businessError);
const result = await finishWorkerPerformanceCapture(capture);
expect(
result?.phaseEvents?.map(({ boundary, metadata }) => ({
boundary,
metadata,
}))
).toEqual([
{ boundary: 'start', metadata: undefined },
{ boundary: 'end', metadata: undefined },
]);
});
});
@@ -0,0 +1,172 @@
import {
PERFORMANCE_PHASE_BOUNDARY,
type AppPlaylistPerformancePhase,
type PerformancePhaseEvent,
type PerformancePhaseMetadata,
} from '@iptvnator/shared/interfaces';
import type { WorkerPerformanceCapture } from './worker-performance-capture.model';
import { safeReadEpochMs } from './worker-performance-capture.runtime';
export function captureWorkerPerformancePhase<TResult>(
capture: WorkerPerformanceCapture | null,
phase: AppPlaylistPerformancePhase,
execute: () => TResult,
metadata?: (result: TResult) => PerformancePhaseMetadata
): TResult {
if (!capture || !capture.requestId) {
return execute();
}
const startMonotonicMs = safeReadMonotonicMs(capture);
appendPhaseEvent(capture, {
boundary: PERFORMANCE_PHASE_BOUNDARY.START,
durationMs: null,
epochMs: safeReadEpochMs(capture.runtime),
metadata: undefined,
phase,
requestId: capture.requestId,
});
try {
const result = execute();
const endMonotonicMs = safeReadMonotonicMs(capture);
appendPhaseEvent(capture, {
boundary: PERFORMANCE_PHASE_BOUNDARY.END,
durationMs: safeDuration(endMonotonicMs - startMonotonicMs),
epochMs: safeReadEpochMs(capture.runtime),
metadata: sanitizeMetadata(safelyCreateMetadata(metadata, result)),
phase,
requestId: capture.requestId,
});
return result;
} catch (error) {
const endMonotonicMs = safeReadMonotonicMs(capture);
appendPhaseEvent(capture, {
boundary: PERFORMANCE_PHASE_BOUNDARY.END,
durationMs: safeDuration(endMonotonicMs - startMonotonicMs),
epochMs: safeReadEpochMs(capture.runtime),
metadata: undefined,
phase,
requestId: capture.requestId,
});
throw error;
}
}
export function captureWorkerPerformancePhaseAsync<TResult>(
capture: WorkerPerformanceCapture | null,
phase: AppPlaylistPerformancePhase,
execute: () => Promise<TResult>,
metadata?: (result: TResult) => PerformancePhaseMetadata
): Promise<TResult> {
const requestId = capture?.requestId;
if (!capture || !requestId) {
return execute();
}
return captureAsync(capture, requestId, phase, execute, metadata);
}
async function captureAsync<TResult>(
capture: WorkerPerformanceCapture,
requestId: string,
phase: AppPlaylistPerformancePhase,
execute: () => Promise<TResult>,
metadata?: (result: TResult) => PerformancePhaseMetadata
): Promise<TResult> {
const startMonotonicMs = safeReadMonotonicMs(capture);
appendPhaseEvent(capture, {
boundary: PERFORMANCE_PHASE_BOUNDARY.START,
durationMs: null,
epochMs: safeReadEpochMs(capture.runtime),
metadata: undefined,
phase,
requestId,
});
try {
const result = await execute();
const endMonotonicMs = safeReadMonotonicMs(capture);
appendPhaseEvent(capture, {
boundary: PERFORMANCE_PHASE_BOUNDARY.END,
durationMs: safeDuration(endMonotonicMs - startMonotonicMs),
epochMs: safeReadEpochMs(capture.runtime),
metadata: sanitizeMetadata(
safelyCreateMetadata(metadata, result)
),
phase,
requestId,
});
return result;
} catch (error) {
const endMonotonicMs = safeReadMonotonicMs(capture);
appendPhaseEvent(capture, {
boundary: PERFORMANCE_PHASE_BOUNDARY.END,
durationMs: safeDuration(endMonotonicMs - startMonotonicMs),
epochMs: safeReadEpochMs(capture.runtime),
metadata: undefined,
phase,
requestId,
});
throw error;
}
}
function safeReadMonotonicMs(capture: WorkerPerformanceCapture): number {
try {
const value = capture.runtime.readMonotonicMs();
return Number.isFinite(value) ? value : 0;
} catch {
return 0;
}
}
function safeDuration(value: number): number {
return Number.isFinite(value) ? Math.max(0, value) : 0;
}
function safelyCreateMetadata<TResult>(
createMetadata: ((result: TResult) => PerformancePhaseMetadata) | undefined,
result: TResult
): PerformancePhaseMetadata | undefined {
if (!createMetadata) {
return undefined;
}
try {
return createMetadata(result);
} catch {
return undefined;
}
}
function sanitizeMetadata(
metadata: PerformancePhaseMetadata | undefined
): PerformancePhaseMetadata | undefined {
if (!metadata) {
return undefined;
}
const sanitized: { byteCount?: number; itemCount?: number } = {};
if (isCount(metadata.byteCount)) {
sanitized.byteCount = metadata.byteCount;
}
if (isCount(metadata.itemCount)) {
sanitized.itemCount = metadata.itemCount;
}
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}
function isCount(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
function appendPhaseEvent(
capture: WorkerPerformanceCapture,
event: PerformancePhaseEvent<AppPlaylistPerformancePhase>
): void {
try {
capture.phaseEvents.push(event);
} catch {
// Phase instrumentation is deliberately fail-neutral.
}
}
@@ -0,0 +1,123 @@
import { TestBed } from '@angular/core/testing';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { PlaylistActions } from '@iptvnator/m3u-state';
import { SettingsStore } from '@iptvnator/services';
import {
PLAYLIST_PARSE_BY_URL,
type Playlist,
} from '@iptvnator/shared/interfaces';
import {
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
type RendererPerformancePhaseEvent,
} from '@iptvnator/shared/logging';
import { DialogService } from '@iptvnator/ui/components';
import { ElectronService } from './electron.service';
describe('ElectronService performance phases', () => {
const hookSymbol = Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY);
const electronBridge = {
fetchPlaylistByUrl: jest.fn(),
onPlayerError: jest.fn(),
};
const store = { dispatch: jest.fn() };
let service: ElectronService;
beforeEach(() => {
Object.defineProperty(window, 'electron', {
configurable: true,
value: electronBridge,
});
electronBridge.fetchPlaylistByUrl.mockReset();
electronBridge.onPlayerError.mockReset();
store.dispatch.mockReset();
TestBed.configureTestingModule({
providers: [
ElectronService,
{
provide: DialogService,
useValue: { openConfirmDialog: jest.fn() },
},
{
provide: MatSnackBar,
useValue: { open: jest.fn() },
},
{
provide: SettingsStore,
useValue: {
getTrustOptions: jest.fn(() => ({
trustedInsecureTlsHosts: [],
trustedPrivateNetworkEpgUrls: [],
})),
},
},
{ provide: Store, useValue: store },
{
provide: TranslateService,
useValue: { instant: jest.fn((key: string) => key) },
},
],
});
service = TestBed.inject(ElectronService);
});
afterEach(() => {
delete (globalThis as unknown as Record<symbol, unknown>)[hookSymbol];
Object.defineProperty(window, 'electron', {
configurable: true,
value: undefined,
});
});
it('marks the initial URL import store dispatch without exposing playlist data', async () => {
const events: RendererPerformancePhaseEvent[] = [];
(
globalThis as unknown as Record<
symbol,
(event: RendererPerformancePhaseEvent) => void
>
)[hookSymbol] = (event) => events.push(event);
const playlist = {
playlist: { items: [{ name: 'Sensitive channel' }] },
title: 'Sensitive title',
url: 'https://user:secret@example.test/list.m3u',
} as Playlist;
electronBridge.fetchPlaylistByUrl.mockResolvedValue(playlist);
await service.sendIpcEvent(PLAYLIST_PARSE_BY_URL, {
title: playlist.title,
url: playlist.url,
});
await Promise.resolve();
await Promise.resolve();
expect(store.dispatch).toHaveBeenCalledWith(
PlaylistActions.handleAddingPlaylistByUrl({
isTemporary: false,
playlist,
})
);
expect(
events.map(({ boundary, outcome, phase }) => ({
boundary,
outcome,
phase,
}))
).toEqual([
{
boundary: 'start',
outcome: undefined,
phase: 'store.m3u-import-dispatch',
},
{
boundary: 'end',
outcome: 'success',
phase: 'store.m3u-import-dispatch',
},
]);
expect(JSON.stringify(events)).not.toContain('Sensitive');
expect(JSON.stringify(events)).not.toContain('secret');
});
});
+13 -5
View File
@@ -20,6 +20,10 @@ import {
XTREAM_RESPONSE,
XtreamCodeActions,
} from '@iptvnator/shared/interfaces';
import {
measureRendererPerformancePhase,
RENDERER_PERFORMANCE_PHASE,
} from '@iptvnator/shared/logging';
import { AppConfig } from '../../environments/environment';
import { buildAutoUpdatePlaylistsFeedback } from './auto-update-playlists-feedback';
import {
@@ -316,11 +320,15 @@ export class ElectronService extends DataService {
this.settingsStore.getTrustOptions()
)
.then((result) => {
this.store.dispatch(
PlaylistActions.handleAddingPlaylistByUrl({
isTemporary: !!payload?.isTemporary,
playlist: result,
})
measureRendererPerformancePhase(
RENDERER_PERFORMANCE_PHASE.M3U_IMPORT_DISPATCH,
() =>
this.store.dispatch(
PlaylistActions.handleAddingPlaylistByUrl({
isTemporary: !!payload?.isTemporary,
playlist: result,
})
)
);
})
.catch((error: unknown) => {
+89
View File
@@ -56,6 +56,95 @@ There is intentionally **no URL validation** (upstream removed it in 0.15.0): an
The behavioral contract is guarded by `apps/web/src/app/iptv-playlist-parser.contract.spec.ts` (jest maps the module to the real parser source) and by the fork's own test suite.
## Initial URL Import Performance Benchmark (Electron)
The Electron E2E project has a deterministic initial-import benchmark for
10,000-, 50,000-, and 100,000-channel M3U playlists. It uses only generated
fixtures served by an ephemeral `127.0.0.1` HTTP server; provider URLs,
credentials, and playlist data must never be used. Each formal scenario runs
one warm-up, five measured iterations, and one diagnostic iteration in a fresh
Electron process and data directory:
```bash
perf_output="$PWD/dist/performance/$(date -u +%Y%m%dT%H%M%SZ)-m3u-import"
IPTVNATOR_PERF_OUTPUT_DIR="$perf_output" \
IPTVNATOR_PERF_VARIANT=baseline \
pnpm nx run electron-backend-e2e:benchmark-m3u-import
```
Use a new output directory and `IPTVNATOR_PERF_VARIANT=after` for the identical
post-change run. Formal runs require a clean worktree and record the commit,
source-state hash, runtime, and exact fixture identity. A development smoke run
uses one warm-up, one measured iteration, and one diagnostic iteration per
size:
```bash
IPTVNATOR_PERF_OUTPUT_DIR="$PWD/dist/performance/<timestamp>-m3u-import-smoke" \
IPTVNATOR_PERF_VARIANT=smoke \
IPTVNATOR_PERF_SMOKE=1 \
pnpm nx run electron-backend-e2e:benchmark-m3u-import
```
Smoke runs may use a dirty worktree and validate only the harness; they cannot
support a performance claim. If another local application owns port 9222, smoke
only may set `IPTVNATOR_PERF_CDP_PORT` to an unused loopback port; formal runs
fail closed unless CDP uses `127.0.0.1:9222`. Raw captures and JSON results stay
under the gitignored `dist/performance/` tree; preflight rejects a symbolic
link in any existing output-path component before creating artifacts. Headline
distributions contain only the five measured runs: warm-up and diagnostic
profiles are never mixed into them.
The benchmark attributes these non-additive intervals:
| Field | Boundary |
| --- | --- |
| `dataAcquireMs` | loopback response acquisition |
| `m3uParsingMs` | parser call |
| `normalizationMs` | parsed-item normalization |
| `mainToRendererCloneProxyMs` | sum of normalized import-result delivery plus the upsert and GET main-response-to-preload-success legs |
| `storeImportDispatchMs` | renderer import dispatch |
| `rendererToMainCloneProxyMs` | sum of the upsert and GET preload-source-to-main-request legs |
| `mainToDatabaseWorkerCloneProxyMs` | sum of the upsert and GET main-request-to-worker-receive legs |
| `playlistSerializationMs` | database-worker playlist JSON serialization |
| `sqliteWriteMs` | SQLite upsert, including its autocommit |
| `sqliteReadMs` | SQLite read of the newly persisted playlist |
| `playlistDeserializationMs` | database-worker `parseAppPlaylist`, including playlist JSON parsing |
| `databaseWorkerToMainCloneProxyMs` | sum of the upsert and GET worker-response-post-to-main-response legs |
| `storePublishChannelsMs` | renderer channel publication |
| `angularRenderingMs` | publication end to the terminal two-frame paint proof |
`ipcStructuredCloneProxyMs` is the explicit sum of the four directional proxy
fields. Each directional field can combine the applicable initial-result,
upsert, and `DB_GET_APP_PLAYLIST` legs. The worker stamps
`responsePostedEpochMs` after request profiling is finalized and immediately
before each response is posted; the database-worker-to-main proxy ends when
main receives that response. These fields are attribution aids, not an additive
waterfall: scheduler work and gaps can remain inside total wall time, and a
proxy can include response construction, dispatch overhead, and structured
clone work. Initial import does not create indexes; `indexAndCommitMs` is
therefore `N/A` with
`indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write`.
Instrumentation is development-only, opt-in, fail-neutral, and count-only. It
must not scan or log playlist payloads to generate metadata. Renderer
long-task, frame-gap, and heartbeat samples are clipped to the measured
operation boundary. Main capture stops only after the upsert and route-reload
GET responses plus both asynchronous preload success markers have arrived, so
return-clone attribution cannot race capture shutdown. Formal comparison fails
closed after writing raw results if exact-window renderer RSS, database-worker
peak heap/external samples, or the database worker's explicit post-GC heap is
missing or incoherent. Both initial-import database requests in every measured
run must also contain coherent event-loop delay, event-loop utilization, and
thread-CPU metrics; the validity record exposes the exact expected and valid
request counts rather than silently dropping nullable samples. An iteration
failure idempotently stops renderer timers, closes trace listeners/output, and
starts best-effort probe/session teardown without waiting on a wedged renderer
before Electron is closed. A partial capture-start failure performs the same
rollback before it escapes to the benchmark lifecycle.
Diagnostic artifacts require separate renderer, main, and database-worker CPU
profiles plus renderer/main/database-worker heap snapshots and a Chromium
trace; raw profiles remain ignored.
## Playlist Refresh And Startup Auto-Update (Electron)
Two paths re-download an M3U playlist from its original source:
+42
View File
@@ -128,6 +128,8 @@ Each enabled request gets a fresh event-loop-delay histogram and records:
- `requestReceivedEpochMs`, `workStartedEpochMs`, `workEndedEpochMs`, and
`histogramFlushedEpochMs`
- `responsePostedEpochMs`, sampled after profiling finalization and immediately
before the worker posts the response to main
- worker-thread CPU user/system microseconds from `process.threadCpuUsage()`
- event-loop utilization across the exact work interval
- event-loop-delay max/p95/p99 from the request's own histogram
@@ -146,6 +148,46 @@ profiling queue. If captures overlap, every overlapping response carries
`invalidReason: "overlapping-database-worker-requests"` and all attributable
CPU, ELU, and event-loop-delay values are `null`. This avoids assigning shared
worker activity to one request while preserving normal worker concurrency.
`responsePostedEpochMs` remains a separate response boundary: the initial M3U
benchmark subtracts it from main's response receipt to attribute the
database-worker-to-main structured-clone proxy without folding that interval
into worker execution.
Initial M3U import profiling requires exact operation-specific phase pairs:
- `DB_UPSERT_APP_PLAYLIST`: `serialize.playlist`, then `sqlite.write`. The
first covers playlist JSON serialization; the second covers the SQLite
upsert and its autocommit.
- `DB_GET_APP_PLAYLIST`: `sqlite.read`, then `deserialize.playlist`. The first
covers the awaited single-row SQLite select; the second covers
`parseAppPlaylist`, including JSON parsing and persisted-field hydration.
Missing, partial, reordered, or cross-operation phase sequences fail closed.
Markers carry item counts only and do not scan or copy the payload to compute
profiling metadata. The import creates no indexes, so there is no separate
index/transaction-commit phase. Across the upsert and GET requests, the formal
benchmark reports renderer-to-main, main-to-database-worker,
database-worker-to-main, and main-to-renderer structured-clone proxies; their
explicit sum is `ipcStructuredCloneProxyMs`. See
[M3U Playlist Module Architecture](./m3u-playlist-module.md#initial-url-import-performance-benchmark-electron)
for the complete cross-process attribution.
Formal initial-import comparison also requires both request-scoped captures in
every measured run to have coherent event-loop delay, event-loop utilization,
and worker-thread CPU values with no unavailable or invalid reason. Summary
validity records the exact expected and valid request counts; nullable metrics
remain in raw results but cannot be silently omitted from comparison
distributions.
The main-process benchmark samples the database worker's V8
`used_heap_size` and `external_memory` independently. Raw output includes a
valid-sample count for each metric. A peak is numeric only after at least one
finite, non-negative isolate sample; otherwise it is `null` with a fixed
unavailability reason. An initialized zero is never used as evidence of a
successful sample. Formal initial-import comparisons require valid peak
samples from exactly one database worker in every measured run. Worker RSS is
not available per thread and remains part of the separately reported Electron
main-process RSS together with native and SQLite memory.
### Opt-in post-GC heap capture
@@ -0,0 +1,123 @@
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
import { PlaylistsService } from '@iptvnator/services';
import { type Channel, type Playlist } from '@iptvnator/shared/interfaces';
import {
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
type RendererPerformancePhaseEvent,
} from '@iptvnator/shared/logging';
import { of, Subject } from 'rxjs';
import { M3uWorkspaceRouteSession } from './m3u-workspace-route-session.service';
const PLAYLIST_ID = 'performance-playlist';
const PRIMARY_CHANNEL = {
id: 'channel-1',
name: 'Sensitive channel',
url: 'https://stream-secret.example.test/1',
} as Channel;
const NEXT_CHANNEL = {
id: 'channel-2',
name: 'Next sensitive channel',
url: 'https://stream-secret.example.test/2',
} as Channel;
async function flushEffects(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
}
describe('M3uWorkspaceRouteSession performance phases', () => {
const hookSymbol = Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY);
const store = { dispatch: jest.fn() };
beforeEach(() => {
Object.defineProperty(window, 'electron', {
configurable: true,
value: { setUserAgent: jest.fn().mockResolvedValue(true) },
});
store.dispatch.mockReset();
TestBed.configureTestingModule({
providers: [
M3uWorkspaceRouteSession,
{
provide: PlaylistContextFacade,
useValue: {
syncFromUrl: jest.fn(() => ({
inWorkspace: true,
playlistId: PLAYLIST_ID,
provider: 'playlists',
section: 'all',
})),
},
},
{
provide: PlaylistsService,
useValue: {
getPlaylist: jest.fn(() =>
of({
playlist: {
items: [PRIMARY_CHANNEL, NEXT_CHANNEL],
},
title: 'Sensitive title',
url: 'https://user:secret@example.test/list.m3u',
} as Playlist)
),
},
},
{
provide: Router,
useValue: {
events: new Subject().asObservable(),
url: `/workspace/playlists/${PLAYLIST_ID}/all`,
},
},
{ provide: Store, useValue: store },
],
});
});
afterEach(() => {
delete (globalThis as unknown as Record<symbol, unknown>)[hookSymbol];
delete (window as unknown as { electron?: unknown }).electron;
});
it('marks channel publication with only the imported item count', async () => {
const events: RendererPerformancePhaseEvent[] = [];
(
globalThis as unknown as Record<
symbol,
(event: RendererPerformancePhaseEvent) => void
>
)[hookSymbol] = (event) => events.push(event);
TestBed.inject(M3uWorkspaceRouteSession);
await flushEffects();
expect(
events.map(({ boundary, metadata, outcome, phase }) => ({
boundary,
metadata,
outcome,
phase,
}))
).toEqual([
{
boundary: 'start',
metadata: { items: 2 },
outcome: undefined,
phase: 'store.m3u-publish-channels',
},
{
boundary: 'end',
metadata: { items: 2 },
outcome: 'success',
phase: 'store.m3u-publish-channels',
},
]);
expect(JSON.stringify(events)).not.toContain('Sensitive');
expect(JSON.stringify(events)).not.toContain('secret');
expect(JSON.stringify(events)).not.toContain(PRIMARY_CHANNEL.url);
});
});
@@ -9,6 +9,10 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { ChannelActions, FavoritesActions } from '@iptvnator/m3u-state';
import {
measureRendererPerformancePhase,
RENDERER_PERFORMANCE_PHASE,
} from '@iptvnator/shared/logging';
import { filter, firstValueFrom } from 'rxjs';
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
import { PlaylistsService } from '@iptvnator/services';
@@ -107,10 +111,16 @@ export class M3uWorkspaceRouteSession {
);
});
this.store.dispatch(
ChannelActions.setChannels({
channels: playlist.playlist?.items ?? [],
})
const channels = playlist.playlist?.items ?? [];
measureRendererPerformancePhase(
RENDERER_PERFORMANCE_PHASE.M3U_PUBLISH_CHANNELS,
() =>
this.store.dispatch(
ChannelActions.setChannels({
channels,
})
),
() => ({ items: channels.length })
);
const favorites = (playlist.favorites ?? []).filter(
+1
View File
@@ -19,6 +19,7 @@ export * from './lib/ipc-commands';
export * from './lib/language.enum';
export * from './lib/m3u-favorite-channel.interface';
export * from './lib/parsed-playlist.interface';
export * from './lib/performance-phase.interface';
export * from './lib/playback-position.interface';
export * from './lib/playlist-auto-update.interface';
export * from './lib/playlist-backup.interface';
@@ -0,0 +1,53 @@
export const PERFORMANCE_PHASE_BOUNDARY = {
END: 'end',
START: 'start',
} as const;
export type PerformancePhaseBoundary =
(typeof PERFORMANCE_PHASE_BOUNDARY)[keyof typeof PERFORMANCE_PHASE_BOUNDARY];
export const M3U_IMPORT_PERFORMANCE_PHASE_EVENT_CHANNEL =
'IPTVNATOR_M3U_IMPORT_PERFORMANCE_PHASE_EVENT';
export const M3U_IMPORT_PERFORMANCE_PHASE = {
DATA_ACQUIRE: 'data.acquire',
NORMALIZE: 'normalize',
PARSE_M3U: 'parse.m3u',
} as const;
export type M3uImportPerformancePhase =
(typeof M3U_IMPORT_PERFORMANCE_PHASE)[keyof typeof M3U_IMPORT_PERFORMANCE_PHASE];
export const APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE = {
SERIALIZE_PLAYLIST: 'serialize.playlist',
SQLITE_WRITE: 'sqlite.write',
} as const;
export type AppPlaylistUpsertPerformancePhase =
(typeof APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE)[keyof typeof APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE];
export const APP_PLAYLIST_GET_PERFORMANCE_PHASE = {
DESERIALIZE_PLAYLIST: 'deserialize.playlist',
SQLITE_READ: 'sqlite.read',
} as const;
export type AppPlaylistGetPerformancePhase =
(typeof APP_PLAYLIST_GET_PERFORMANCE_PHASE)[keyof typeof APP_PLAYLIST_GET_PERFORMANCE_PHASE];
export type AppPlaylistPerformancePhase =
| AppPlaylistGetPerformancePhase
| AppPlaylistUpsertPerformancePhase;
export interface PerformancePhaseMetadata {
readonly byteCount?: number;
readonly itemCount?: number;
}
export interface PerformancePhaseEvent<TPhase extends string = string> {
readonly boundary: PerformancePhaseBoundary;
readonly durationMs: number | null;
readonly epochMs: number;
readonly metadata?: PerformancePhaseMetadata;
readonly phase: TPhase;
readonly requestId: string;
}
+13
View File
@@ -3,3 +3,16 @@ export {
redactSensitiveData,
} from './lib/redact-sensitive-data';
export type { RedactionOptions } from './lib/redact-sensitive-data';
export {
measureRendererPerformancePhase,
RENDERER_PERFORMANCE_PHASE,
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
} from './lib/renderer-performance-phase';
export type {
RendererPerformancePhase,
RendererPerformancePhaseBoundary,
RendererPerformancePhaseEvent,
RendererPerformancePhaseHook,
RendererPerformancePhaseMetadata,
RendererPerformancePhaseOutcome,
} from './lib/renderer-performance-phase';
@@ -0,0 +1,199 @@
import * as sharedLogging from '../index';
type RendererPerformancePhase =
'store.m3u-import-dispatch' | 'store.m3u-publish-channels';
type RendererPerformancePhaseOutcome = 'error' | 'success';
interface RendererPerformancePhaseEvent {
readonly boundary: 'end' | 'start';
readonly epochMs: number;
readonly metadata?: Readonly<{ items: number }>;
readonly monotonicMs: number;
readonly outcome?: RendererPerformancePhaseOutcome;
readonly phase: RendererPerformancePhase;
readonly phaseId: string;
readonly schemaVersion: 1;
}
type RendererPerformancePhaseHook = (
event: RendererPerformancePhaseEvent
) => void;
interface RendererPerformancePhaseModule {
readonly measureRendererPerformancePhase?: <T>(
phase: RendererPerformancePhase,
execute: () => T,
metadata?: () => Readonly<{ items: number }>
) => T;
readonly RENDERER_PERFORMANCE_PHASE?: Readonly<{
M3U_IMPORT_DISPATCH: 'store.m3u-import-dispatch';
M3U_PUBLISH_CHANNELS: 'store.m3u-publish-channels';
}>;
readonly RENDERER_PERFORMANCE_PHASE_HOOK_KEY?: string;
}
const rendererPerformance = sharedLogging as RendererPerformancePhaseModule;
const EXPECTED_HOOK_KEY = 'iptvnator.renderer.performance-phase-hook.v1';
const hookSymbol = Symbol.for(EXPECTED_HOOK_KEY);
function measure<T>(
phase: RendererPerformancePhase,
execute: () => T,
metadata?: () => Readonly<{ items: number }>
): T {
const measurePhase = rendererPerformance.measureRendererPerformancePhase;
expect(typeof measurePhase).toBe('function');
if (measurePhase === undefined) {
throw new Error('renderer performance phase helper is unavailable');
}
return measurePhase(phase, execute, metadata);
}
function setHook(hook: RendererPerformancePhaseHook | null): void {
const target = globalThis as unknown as Record<symbol, unknown>;
if (hook === null) {
delete target[hookSymbol];
return;
}
target[hookSymbol] = hook;
}
describe('renderer performance phases', () => {
afterEach(() => {
setHook(null);
jest.restoreAllMocks();
});
it('does not evaluate metadata while the global opt-in hook is absent', () => {
const metadata = jest.fn(() => ({ items: 3 }));
const resultIdentity = { imported: true };
const result = measure(
'store.m3u-publish-channels',
() => resultIdentity,
metadata
);
expect(result).toBe(resultIdentity);
expect(metadata).not.toHaveBeenCalled();
});
it('emits an immutable successful pair with epoch and monotonic timestamps', () => {
const events: RendererPerformancePhaseEvent[] = [];
setHook((event) => events.push(event));
const resultIdentity = { imported: true };
const result = measure(
'store.m3u-publish-channels',
() => resultIdentity,
() => ({ items: 3 })
);
expect(result).toBe(resultIdentity);
expect(events).toHaveLength(2);
expect(events[0]).toMatchObject({
boundary: 'start',
metadata: { items: 3 },
phase: 'store.m3u-publish-channels',
schemaVersion: 1,
});
expect(events[1]).toMatchObject({
boundary: 'end',
metadata: { items: 3 },
outcome: 'success',
phase: 'store.m3u-publish-channels',
schemaVersion: 1,
});
expect(events[1].phaseId).toBe(events[0].phaseId);
expect(events[0].epochMs).toEqual(expect.any(Number));
expect(events[0].monotonicMs).toEqual(expect.any(Number));
expect(events[1].epochMs).toBeGreaterThanOrEqual(events[0].epochMs);
expect(events[1].monotonicMs).toBeGreaterThanOrEqual(
events[0].monotonicMs
);
expect(Object.isFrozen(events[0])).toBe(true);
expect(Object.isFrozen(events[0].metadata)).toBe(true);
expect(events[1].metadata).toBe(events[0].metadata);
});
it('emits an error boundary and rethrows the exact product error', () => {
const events: RendererPerformancePhaseEvent[] = [];
setHook((event) => events.push(event));
const productError = new Error('dispatch failed');
let thrown: unknown;
try {
measure('store.m3u-import-dispatch', () => {
throw productError;
});
} catch (error: unknown) {
thrown = error;
}
expect(thrown).toBe(productError);
expect(events).toHaveLength(2);
expect(events[1]).toMatchObject({
boundary: 'end',
outcome: 'error',
phase: 'store.m3u-import-dispatch',
});
expect(events[1].phaseId).toBe(events[0].phaseId);
});
it('keeps product results and errors unchanged when metadata or hooks fail', () => {
setHook(() => {
throw new Error('benchmark hook failed');
});
const resultIdentity = { imported: true };
expect(
measure(
'store.m3u-publish-channels',
() => resultIdentity,
() => {
throw new Error('metadata failed');
}
)
).toBe(resultIdentity);
const productError = new Error('product failed');
let thrown: unknown;
try {
measure('store.m3u-import-dispatch', () => {
throw productError;
});
} catch (error: unknown) {
thrown = error;
}
expect(thrown).toBe(productError);
});
it('exposes only the fixed phase vocabulary and item-count metadata', () => {
expect(rendererPerformance.RENDERER_PERFORMANCE_PHASE_HOOK_KEY).toBe(
EXPECTED_HOOK_KEY
);
expect(rendererPerformance.RENDERER_PERFORMANCE_PHASE).toEqual({
M3U_IMPORT_DISPATCH: 'store.m3u-import-dispatch',
M3U_PUBLISH_CHANNELS: 'store.m3u-publish-channels',
});
const events: RendererPerformancePhaseEvent[] = [];
setHook((event) => events.push(event));
measure(
'store.m3u-publish-channels',
() => undefined,
() =>
({
items: 2,
playlistUrl: 'https://user:secret@example.test/list.m3u',
}) as { items: number }
);
expect(events.map((event) => event.metadata)).toEqual([
{ items: 2 },
{ items: 2 },
]);
expect(JSON.stringify(events)).not.toContain('secret');
expect(JSON.stringify(events)).not.toContain('playlistUrl');
});
});
@@ -0,0 +1,181 @@
export const RENDERER_PERFORMANCE_PHASE_HOOK_KEY =
'iptvnator.renderer.performance-phase-hook.v1';
export const RENDERER_PERFORMANCE_PHASE = {
M3U_IMPORT_DISPATCH: 'store.m3u-import-dispatch',
M3U_PUBLISH_CHANNELS: 'store.m3u-publish-channels',
} as const;
export type RendererPerformancePhase =
(typeof RENDERER_PERFORMANCE_PHASE)[keyof typeof RENDERER_PERFORMANCE_PHASE];
export type RendererPerformancePhaseBoundary = 'end' | 'start';
export type RendererPerformancePhaseOutcome = 'error' | 'success';
export interface RendererPerformancePhaseMetadata {
readonly items: number;
}
export interface RendererPerformancePhaseEvent {
readonly boundary: RendererPerformancePhaseBoundary;
readonly epochMs: number;
readonly metadata?: Readonly<RendererPerformancePhaseMetadata>;
readonly monotonicMs: number;
readonly outcome?: RendererPerformancePhaseOutcome;
readonly phase: RendererPerformancePhase;
readonly phaseId: string;
readonly schemaVersion: 1;
}
export type RendererPerformancePhaseHook = (
event: RendererPerformancePhaseEvent
) => void;
interface PhaseStart {
readonly epochMs: number;
readonly metadata?: Readonly<RendererPerformancePhaseMetadata>;
readonly monotonicMs: number;
readonly phase: RendererPerformancePhase;
readonly phaseId: string;
}
interface PhaseTimestamp {
readonly epochMs: number;
readonly monotonicMs: number;
}
const hookSymbol = Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY);
let nextPhaseId = 0;
export function measureRendererPerformancePhase<T>(
phase: RendererPerformancePhase,
execute: () => T,
createMetadata?: () => RendererPerformancePhaseMetadata
): T {
const hook = readHook();
if (hook === null) {
return execute();
}
const start = beginPhase(phase, createMetadata);
emit(hook, {
boundary: 'start',
epochMs: start.epochMs,
metadata: start.metadata,
monotonicMs: start.monotonicMs,
phase: start.phase,
phaseId: start.phaseId,
schemaVersion: 1,
});
try {
const result = execute();
endPhase(hook, start, 'success');
return result;
} catch (error) {
endPhase(hook, start, 'error');
throw error;
}
}
function beginPhase(
phase: RendererPerformancePhase,
createMetadata: (() => RendererPerformancePhaseMetadata) | undefined
): PhaseStart {
const timestamp = sampleTimestamp();
return {
...timestamp,
metadata: readMetadata(createMetadata),
phase,
phaseId: `renderer-performance-${++nextPhaseId}`,
};
}
function endPhase(
hook: RendererPerformancePhaseHook,
start: PhaseStart,
outcome: RendererPerformancePhaseOutcome
): void {
const sampled = sampleTimestamp();
emit(hook, {
boundary: 'end',
epochMs: Math.max(start.epochMs, sampled.epochMs),
metadata: start.metadata,
monotonicMs: Math.max(start.monotonicMs, sampled.monotonicMs),
outcome,
phase: start.phase,
phaseId: start.phaseId,
schemaVersion: 1,
});
}
function readHook(): RendererPerformancePhaseHook | null {
try {
const hook = (globalThis as unknown as Record<symbol, unknown>)[
hookSymbol
];
return typeof hook === 'function'
? (hook as RendererPerformancePhaseHook)
: null;
} catch {
return null;
}
}
function readMetadata(
createMetadata: (() => RendererPerformancePhaseMetadata) | undefined
): Readonly<RendererPerformancePhaseMetadata> | undefined {
if (createMetadata === undefined) {
return undefined;
}
try {
const items = createMetadata().items;
return Number.isSafeInteger(items) && items >= 0
? Object.freeze({ items })
: undefined;
} catch {
return undefined;
}
}
function emit(
hook: RendererPerformancePhaseHook,
event: RendererPerformancePhaseEvent
): void {
try {
hook(Object.freeze(event));
} catch {
// Development-only benchmark instrumentation must stay fail-neutral.
}
}
function sampleTimestamp(): PhaseTimestamp {
const fallbackMs = readDateNow();
try {
const monotonicMs = globalThis.performance?.now();
const timeOrigin = globalThis.performance?.timeOrigin;
if (!Number.isFinite(monotonicMs)) {
return { epochMs: fallbackMs, monotonicMs: fallbackMs };
}
const epochMs =
Number.isFinite(timeOrigin) && timeOrigin !== undefined
? timeOrigin + monotonicMs
: fallbackMs;
return {
epochMs: Number.isFinite(epochMs) ? epochMs : fallbackMs,
monotonicMs,
};
} catch {
return { epochMs: fallbackMs, monotonicMs: fallbackMs };
}
}
function readDateNow(): number {
try {
const value = Date.now();
return Number.isFinite(value) ? value : 0;
} catch {
return 0;
}
}