Merge pull request #1275 from 4gray/agent/m3u-renderer-performance

test(perf): add request-scoped M3U benchmark profiling
This commit is contained in:
4gray authored and GitHub committed 2026-07-27 00:07:56 +02:00
commit 75c45c9e91
62 files changed
+7126 -421

No files matched your search

+2 -1
View File
@@ -84,7 +84,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_WORKER_PROFILING=1` enables development/test-only event-loop metrics in database and playlist-refresh worker responses; the performance benchmark sets it automatically, and production launches must leave it unset
- `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; 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 -1
View File
@@ -141,7 +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_WORKER_PROFILING=1` enables development/test-only event-loop metrics in database and playlist-refresh worker responses; the performance benchmark sets it automatically, and production launches must leave it unset
- `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; 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
+9 -2
View File
@@ -12,14 +12,21 @@
"e2e": {
"dependsOn": ["electron-backend:build-e2e"]
},
"test-performance-harness": {
"executor": "nx:run-commands",
"options": {
"cwd": "apps/electron-backend-e2e",
"command": "pnpm exec tsx --test src/performance/*.spec.ts"
}
},
"benchmark-m3u-refresh-cancellation": {
"dependsOn": ["electron-backend:build-e2e"],
"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"
"command": "pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-refresh-cancellation.performance.ts"
}
},
"packaged-frame-copy-smoke": {
@@ -0,0 +1,229 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
import {
installDatabaseRequestIdentityCaptureInMain,
type DatabaseRequestIdentityCaptureApi,
} from './database-request-identity-capture';
const CHANNEL = 'IPTVNATOR_PERF_CAPTURE_MARKER';
let nextStateId = 1;
function createCapture(): {
api: DatabaseRequestIdentityCaptureApi;
ipcMain: EventEmitter;
} {
const ipcMain = new EventEmitter();
const stateKey = `__identityCapture${nextStateId}`;
nextStateId += 1;
installDatabaseRequestIdentityCaptureInMain({ ipcMain } as never, stateKey);
const api = (globalThis as unknown as Record<string, unknown>)[
stateKey
] as DatabaseRequestIdentityCaptureApi;
api.start();
return { api, ipcMain };
}
function marker(overrides: Record<string, unknown> = {}) {
return {
correlationState: 'correlated',
invalidReason: null,
ipcCallId: 2,
method: 'dbGetAppPlaylist',
operationId: 'refresh-operation-1',
phase: 'start',
playlistId: 'playlist-1',
sourceEpochMs: 100,
...overrides,
};
}
test('correlates the real preload marker to a DB request whose payload omits operationId', () => {
const { api, ipcMain } = createCapture();
ipcMain.emit(CHANNEL, {}, marker());
const identity = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-1',
type: 'request',
});
assert.deepEqual(identity, {
operationId: 'refresh-operation-1',
operationIdUnavailableReason: null,
});
});
test('resolves a response-retained request identity when the marker arrives later and resets between captures', () => {
const { api, ipcMain } = createCapture();
const identity = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-before-marker',
type: 'request',
});
assert.equal(
identity.operationIdUnavailableReason,
'preload-performance-marker-missing'
);
const responseOutcome = { identity };
ipcMain.emit(CHANNEL, {}, marker());
assert.deepEqual(responseOutcome.identity, {
operationId: 'refresh-operation-1',
operationIdUnavailableReason: null,
});
api.stop();
ipcMain.emit(CHANNEL, {}, marker({ operationId: 'marker-while-stopped' }));
api.start();
const afterReset = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-after-reset',
type: 'request',
});
assert.deepEqual(afterReset, {
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-missing',
});
});
test('fails closed for missing and ambiguous preload markers', () => {
const missingCapture = createCapture();
const missing = missingCapture.api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-missing',
type: 'request',
});
assert.deepEqual(missing, {
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-missing',
});
const ambiguousCapture = createCapture();
ambiguousCapture.ipcMain.emit(CHANNEL, {}, marker());
ambiguousCapture.ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 3,
operationId: 'refresh-operation-2',
})
);
const ambiguous = ambiguousCapture.api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-ambiguous',
type: 'request',
});
assert.deepEqual(ambiguous, {
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-ambiguous',
});
});
test('quarantines a key after response-before-marker ambiguity', () => {
const { api, ipcMain } = createCapture();
const requestA = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-a',
type: 'request',
});
const requestB = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-b',
type: 'request',
});
ipcMain.emit(
CHANNEL,
{},
marker({ ipcCallId: 2, operationId: 'refresh-operation-a' })
);
ipcMain.emit(
CHANNEL,
{},
marker({ ipcCallId: 3, operationId: 'refresh-operation-b' })
);
const requestC = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-c',
type: 'request',
});
ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 4,
operationId: 'other-playlist-operation',
playlistId: 'playlist-2',
})
);
const otherPlaylist = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-2' },
requestId: 'other-playlist-request',
type: 'request',
});
ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 5,
method: 'dbUpsertAppPlaylist',
operationId: 'other-db-operation',
})
);
const otherOperation = api.matchDatabaseRequest({
operation: 'DB_UPSERT_APP_PLAYLIST',
payload: { _id: 'playlist-1' },
requestId: 'other-operation-request',
type: 'request',
});
const ambiguousIdentity = {
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-ambiguous',
};
assert.deepEqual(requestA, ambiguousIdentity);
assert.deepEqual(requestB, ambiguousIdentity);
assert.deepEqual(requestC, ambiguousIdentity);
assert.deepEqual(otherPlaylist, {
operationId: 'other-playlist-operation',
operationIdUnavailableReason: null,
});
assert.deepEqual(otherOperation, {
operationId: 'other-db-operation',
operationIdUnavailableReason: null,
});
api.stop();
api.start();
ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 6,
operationId: 'fresh-capture-operation',
})
);
const freshCaptureIdentity = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'fresh-capture-request',
type: 'request',
});
assert.deepEqual(freshCaptureIdentity, {
operationId: 'fresh-capture-operation',
operationIdUnavailableReason: null,
});
});
@@ -0,0 +1,270 @@
import type { ElectronApplication } from '@playwright/test';
export const DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY =
'__iptvnatorM3uRefreshDatabaseRequestIdentityCapture';
export interface DatabaseRequestIdentity {
operationId: string | null;
operationIdUnavailableReason: string | null;
}
export interface DatabaseRequestIdentityCaptureApi {
matchDatabaseRequest(message: unknown): DatabaseRequestIdentity;
start(): void;
stop(): void;
}
interface DatabaseRequestIdentityElectron {
readonly ipcMain: {
on(
channel: string,
listener: (event: unknown, marker: unknown) => void
): void;
};
}
export function installDatabaseRequestIdentityCaptureInMain(
{ ipcMain }: DatabaseRequestIdentityElectron,
stateKey: string
): void {
type JsonRecord = Record<string, unknown>;
interface PendingMarker {
readonly ipcCallId: number;
readonly operation: string;
readonly operationId: string;
readonly playlistId: string;
}
interface PendingRequest {
readonly identity: DatabaseRequestIdentity;
readonly operation: string;
readonly playlistId: string;
}
interface QuarantinedKey {
readonly operation: string;
readonly playlistId: string;
}
const target = globalThis as unknown as Record<string, unknown>;
if (target[stateKey] !== undefined) {
return;
}
const markerChannel = 'IPTVNATOR_PERF_CAPTURE_MARKER';
const markerMethodToOperation: Readonly<Record<string, string>> = {
dbGetAppPlaylist: 'DB_GET_APP_PLAYLIST',
dbUpsertAppPlaylist: 'DB_UPSERT_APP_PLAYLIST',
};
let active = false;
let pendingMarkers: PendingMarker[] = [];
let pendingRequests: PendingRequest[] = [];
let quarantinedKeys: QuarantinedKey[] = [];
const readRecord = (value: unknown): JsonRecord | null =>
typeof value === 'object' && value !== null
? (value as JsonRecord)
: null;
const readPlaylistId = (
operation: string,
payload: unknown
): string | null => {
const value = readRecord(payload);
if (!value) {
return null;
}
const playlistId =
operation === 'DB_UPSERT_APP_PLAYLIST'
? value['_id']
: value['playlistId'];
return typeof playlistId === 'string' && playlistId.length > 0
? playlistId
: null;
};
const matches = (
candidate: { operation: string; playlistId: string },
operation: string,
playlistId: string
): boolean =>
candidate.operation === operation &&
candidate.playlistId === playlistId;
const markAmbiguous = (requests: PendingRequest[]): void => {
for (const request of requests) {
request.identity.operationId = null;
request.identity.operationIdUnavailableReason =
'preload-performance-marker-ambiguous';
}
};
const isQuarantined = (operation: string, playlistId: string): boolean =>
quarantinedKeys.some((key) => matches(key, operation, playlistId));
const quarantine = (operation: string, playlistId: string): void => {
if (!isQuarantined(operation, playlistId)) {
quarantinedKeys.push({ operation, playlistId });
}
const requestCandidates = pendingRequests.filter((request) =>
matches(request, operation, playlistId)
);
markAmbiguous(requestCandidates);
pendingRequests = pendingRequests.filter(
(request) => !matches(request, operation, playlistId)
);
pendingMarkers = pendingMarkers.filter(
(marker) => !matches(marker, operation, playlistId)
);
};
ipcMain.on(markerChannel, (_event, input) => {
if (!active) {
return;
}
const marker = readRecord(input);
if (
!marker ||
marker['phase'] !== 'start' ||
marker['correlationState'] !== 'correlated' ||
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'];
if (
operation === undefined ||
typeof operationId !== 'string' ||
operationId.length === 0 ||
typeof playlistId !== 'string' ||
playlistId.length === 0 ||
!Number.isSafeInteger(ipcCallId) ||
Number(ipcCallId) < 1
) {
return;
}
if (isQuarantined(operation, playlistId)) {
return;
}
const requestCandidates = pendingRequests.filter((request) =>
matches(request, operation, playlistId)
);
if (requestCandidates.length === 1) {
const request = requestCandidates[0];
if (!request) {
return;
}
request.identity.operationId = operationId;
request.identity.operationIdUnavailableReason = null;
pendingRequests = pendingRequests.filter(
(candidate) => candidate !== request
);
return;
}
if (requestCandidates.length > 1) {
quarantine(operation, playlistId);
return;
}
pendingMarkers.push({
ipcCallId: Number(ipcCallId),
operation,
operationId,
playlistId,
});
pendingMarkers.sort((left, right) => left.ipcCallId - right.ipcCallId);
});
const api: DatabaseRequestIdentityCaptureApi = {
matchDatabaseRequest: (input) => {
const message = readRecord(input);
const operation = message?.['operation'];
const playlistId =
typeof operation === 'string'
? readPlaylistId(operation, message?.['payload'])
: null;
if (
!active ||
(operation !== 'DB_GET_APP_PLAYLIST' &&
operation !== 'DB_UPSERT_APP_PLAYLIST') ||
playlistId === null
) {
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-not-applicable',
};
}
if (isQuarantined(operation, playlistId)) {
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-ambiguous',
};
}
const markerCandidates = pendingMarkers.filter((marker) =>
matches(marker, operation, playlistId)
);
if (markerCandidates.length === 1) {
const marker = markerCandidates[0];
if (!marker) {
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-missing',
};
}
pendingMarkers = pendingMarkers.filter(
(candidate) => candidate !== marker
);
return {
operationId: marker.operationId,
operationIdUnavailableReason: null,
};
}
if (markerCandidates.length > 1) {
quarantine(operation, playlistId);
return {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-ambiguous',
};
}
const identity: DatabaseRequestIdentity = {
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-missing',
};
pendingRequests.push({
identity,
operation,
playlistId,
});
return identity;
},
start: () => {
pendingMarkers = [];
pendingRequests = [];
quarantinedKeys = [];
active = true;
},
stop: () => {
active = false;
pendingMarkers = [];
pendingRequests = [];
quarantinedKeys = [];
},
};
target[stateKey] = api;
}
export async function installDatabaseRequestIdentityCapture(
electronApp: ElectronApplication
): Promise<void> {
await electronApp.evaluate(
installDatabaseRequestIdentityCaptureInMain,
DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY
);
}
@@ -48,6 +48,29 @@ export interface MainTimelineRecord {
readonly type: string;
}
export interface WorkerRequestPerformanceMetrics {
readonly eventLoopDelay: EventLoopDelayMetrics | null;
readonly eventLoopDelayUnavailableReason: string | null;
readonly eventLoopUtilization: number | null;
readonly eventLoopUtilizationUnavailableReason: string | null;
readonly histogramFlushedEpochMs: number | null;
readonly invalidReason: string | null;
readonly operation: string | null;
readonly operationId: string | null;
readonly operationIdUnavailableReason: string | null;
readonly performanceCaptureUnavailableReason: string | null;
readonly playlistId: string | null;
readonly requestId: string | null;
readonly requestReceivedEpochMs: number | null;
readonly responseEpochMs: number;
readonly success: boolean;
readonly threadCpuSystemMicros: number | null;
readonly threadCpuUnavailableReason: string | null;
readonly threadCpuUserMicros: number | null;
readonly workEndedEpochMs: number | null;
readonly workStartedEpochMs: number | null;
}
export interface WorkerCaptureMetrics {
readonly cancelPostedEpochMs: number | null;
readonly cpuSystemMicros: number | null;
@@ -62,6 +85,7 @@ export interface WorkerCaptureMetrics {
readonly playlistId: string | null;
readonly postGcHeapUsedBytes: number | null;
readonly profilePath: string | null;
readonly requests: readonly WorkerRequestPerformanceMetrics[];
readonly responseEpochMs: number | null;
readonly snapshotPath: string | null;
readonly terminatedEpochMs: number | null;
@@ -176,13 +200,15 @@ export interface CancellationBenchmarkSummary {
readonly cancelToDurableTerminalMs: NumericDistribution;
readonly cancelToWorkerTerminatedMs: NumericDistribution;
readonly cancelTransportLatencyMs: NumericDistribution;
readonly databaseWorkerEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerEventLoopUtilization: NumericDistribution;
readonly databaseWorkerExternalPeakBytes: NumericDistribution;
readonly databaseWorkerHeapPeakBytes: NumericDistribution;
readonly databaseWorkerPostGcHeapBytes: NumericDistribution;
readonly databaseWorkerRequestEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerRequestEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerRequestEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerRequestEventLoopUtilization: NumericDistribution;
readonly databaseWorkerRequestThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerRequestThreadCpuUserMicros: NumericDistribution;
readonly dataFetchMs: NumericDistribution;
readonly dbCommitProxyMs: NumericDistribution;
readonly ipcStoreDispatchProxyMs: NumericDistribution;
@@ -196,13 +222,15 @@ export interface CancellationBenchmarkSummary {
readonly mainRssPostGcBytes: NumericDistribution;
readonly parseNormalizeCloneProxyMs: NumericDistribution;
readonly persistencePreparationProxyMs: NumericDistribution;
readonly playlistWorkerEventLoopUtilization: NumericDistribution;
readonly playlistWorkerEventLoopDelayMaxMs: NumericDistribution;
readonly playlistWorkerEventLoopDelayP95Ms: NumericDistribution;
readonly playlistWorkerEventLoopDelayP99Ms: NumericDistribution;
readonly playlistWorkerExternalPeakBytes: NumericDistribution;
readonly playlistWorkerHeapPeakBytes: NumericDistribution;
readonly playlistWorkerPostGcHeapBytes: NumericDistribution;
readonly playlistWorkerRequestEventLoopDelayMaxMs: NumericDistribution;
readonly playlistWorkerRequestEventLoopDelayP95Ms: NumericDistribution;
readonly playlistWorkerRequestEventLoopDelayP99Ms: NumericDistribution;
readonly playlistWorkerRequestEventLoopUtilization: NumericDistribution;
readonly playlistWorkerRequestThreadCpuSystemMicros: NumericDistribution;
readonly playlistWorkerRequestThreadCpuUserMicros: NumericDistribution;
readonly rendererFrameGapMs: NumericDistribution;
readonly rendererHeartbeatDelayMs: NumericDistribution;
readonly rendererHeapPeakBytes: NumericDistribution;
@@ -9,6 +9,7 @@ import type {
NumericDistribution,
PerformanceWorkerKind,
RendererCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
PERFORMANCE_ITERATION_KIND,
@@ -63,15 +64,28 @@ export function createCancellationBenchmarkSummary(
return worker ? select(worker) : null;
})
);
const workerEventLoopDelayMetric = (
const workerRequestMetric = (
kind: PerformanceWorkerKind,
select: (request: WorkerRequestPerformanceMetrics) => number | null
): NumericDistribution =>
summarizeNumbers(
measured.flatMap((iteration) =>
iteration.main.workers
.filter((worker) => worker.kind === kind)
.flatMap((worker) =>
worker.requests.map((request) => select(request))
)
)
);
const workerRequestEventLoopDelayMetric = (
kind: PerformanceWorkerKind,
percentile: keyof NonNullable<
MainCaptureMetrics['workers'][number]['eventLoopDelay']
WorkerRequestPerformanceMetrics['eventLoopDelay']
>
): NumericDistribution =>
workerMetric(
workerRequestMetric(
kind,
(worker) => worker.eventLoopDelay?.[percentile] ?? null
(request) => request.eventLoopDelay?.[percentile] ?? null
);
return Object.freeze({
@@ -105,22 +119,6 @@ export function createCancellationBenchmarkSummary(
(iteration) => iteration.phases.cancelTransportLatencyMs
)
),
databaseWorkerEventLoopDelayMaxMs: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'maxMs'
),
databaseWorkerEventLoopDelayP95Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p95Ms'
),
databaseWorkerEventLoopDelayP99Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p99Ms'
),
databaseWorkerEventLoopUtilization: workerMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(worker) => worker.eventLoopUtilization
),
databaseWorkerExternalPeakBytes: workerMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(worker) => worker.peakExternalBytes
@@ -133,6 +131,33 @@ export function createCancellationBenchmarkSummary(
PERFORMANCE_WORKER_KIND.DATABASE,
(worker) => worker.postGcHeapUsedBytes
),
databaseWorkerRequestEventLoopDelayMaxMs:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'maxMs'
),
databaseWorkerRequestEventLoopDelayP95Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p95Ms'
),
databaseWorkerRequestEventLoopDelayP99Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p99Ms'
),
databaseWorkerRequestEventLoopUtilization: workerRequestMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(request) => request.eventLoopUtilization
),
databaseWorkerRequestThreadCpuSystemMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(request) => request.threadCpuSystemMicros
),
databaseWorkerRequestThreadCpuUserMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(request) => request.threadCpuUserMicros
),
dataFetchMs: summarizeNumbers(
measured.map((iteration) => iteration.phases.dataFetchMs)
),
@@ -185,22 +210,6 @@ export function createCancellationBenchmarkSummary(
iteration.phases.persistencePreparationProxyMs
)
),
playlistWorkerEventLoopUtilization: workerMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(worker) => worker.eventLoopUtilization
),
playlistWorkerEventLoopDelayMaxMs: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'maxMs'
),
playlistWorkerEventLoopDelayP95Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p95Ms'
),
playlistWorkerEventLoopDelayP99Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p99Ms'
),
playlistWorkerExternalPeakBytes: workerMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(worker) => worker.peakExternalBytes
@@ -213,6 +222,33 @@ export function createCancellationBenchmarkSummary(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(worker) => worker.postGcHeapUsedBytes
),
playlistWorkerRequestEventLoopDelayMaxMs:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'maxMs'
),
playlistWorkerRequestEventLoopDelayP95Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p95Ms'
),
playlistWorkerRequestEventLoopDelayP99Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p99Ms'
),
playlistWorkerRequestEventLoopUtilization: workerRequestMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(request) => request.eventLoopUtilization
),
playlistWorkerRequestThreadCpuSystemMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(request) => request.threadCpuSystemMicros
),
playlistWorkerRequestThreadCpuUserMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(request) => request.threadCpuUserMicros
),
rendererFrameGapMs: summarizeNumbers(
flattenRenderer(
(iteration) => iteration.renderer.probe.frameGapsMs
@@ -166,6 +166,7 @@ async function runIteration(
],
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',
@@ -1,7 +1,17 @@
/* eslint-disable max-lines -- The injected main-process protocol must remain self-contained for Playwright serialization. */
import type { ElectronApplication } from '@playwright/test';
import {
DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY,
installDatabaseRequestIdentityCapture,
type DatabaseRequestIdentity,
type DatabaseRequestIdentityCaptureApi,
} from './database-request-identity-capture';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
import {
selectMainCaptureGeneration,
type MainCaptureGenerationTransport,
} from './worker-request-performance';
const MAIN_CAPTURE_STATE_KEY = '__iptvnatorM3uRefreshMainCapture';
@@ -23,7 +33,13 @@ export interface MainCaptureStatus {
export async function installMainCapture(
electronApp: ElectronApplication
): Promise<void> {
await electronApp.evaluate(async ({ app, BrowserWindow }, stateKey) => {
await installDatabaseRequestIdentityCapture(electronApp);
const captureStateKeys = {
databaseRequestIdentityStateKey:
DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY,
stateKey: MAIN_CAPTURE_STATE_KEY,
};
await electronApp.evaluate(async ({ app, BrowserWindow }, input) => {
type JsonRecord = Record<string, unknown>;
type WorkerTransferable = import('node:worker_threads').Transferable;
interface CpuProfileHandle {
@@ -42,6 +58,15 @@ export async function installMainCapture(
idle: number;
utilization: number;
}
interface WorkerRequestPerformance {
identity: DatabaseRequestIdentity;
operation: string | null;
performanceCapture: unknown;
playlistId: string | null;
requestId: string | null;
responseEpochMs: number;
success: boolean;
}
interface InstrumentedWorker {
cpuUsage?(): Promise<WorkerCpuUsage>;
getHeapSnapshot?(): Promise<NodeJS.ReadableStream>;
@@ -62,15 +87,11 @@ export async function installMainCapture(
}
interface WorkerRecord {
cancelPostedEpochMs: number | null;
captureGeneration: number | null;
cpuFirst: WorkerCpuUsage | null;
cpuLast: WorkerCpuUsage | null;
elu: number | null;
eluStart: WorkerElu | null;
eventLoopDelay: {
maxMs: number;
p95Ms: number;
p99Ms: number;
} | null;
externalPeak: number;
finalized: boolean;
finalizing: Promise<void> | null;
@@ -81,6 +102,7 @@ export async function installMainCapture(
postGcHeapUsed: number | null;
profileHandle: Promise<CpuProfileHandle> | null;
profilePath: string | null;
requestPerformance: WorkerRequestPerformance[];
responseEpochMs: number | null;
sampleBusy: boolean;
sampleTimer: NodeJS.Timeout | null;
@@ -99,9 +121,12 @@ export async function installMainCapture(
}
const target = globalThis as unknown as Record<string, unknown>;
if (target[stateKey] !== undefined) {
if (target[input.stateKey] !== undefined) {
return;
}
const databaseRequestIdentityCapture = target[
input.databaseRequestIdentityStateKey
] as DatabaseRequestIdentityCaptureApi;
const runtimeProcess = process as typeof process & {
getBuiltinModule(id: string): unknown;
@@ -132,6 +157,7 @@ export async function installMainCapture(
const dbRequests = new Map<
string,
{
identity: DatabaseRequestIdentity;
operation: string;
playlistId: string | null;
record: WorkerRecord;
@@ -140,6 +166,7 @@ export async function installMainCapture(
const state = {
active: false,
captureGeneration: 0,
cpuStart: null as NodeJS.CpuUsage | null,
diagnostic: false,
eventLoopDelay: null as ReturnType<
@@ -200,6 +227,9 @@ export async function installMainCapture(
const playlistId = value['playlistId'] ?? value['_id'];
return typeof playlistId === 'string' ? playlistId : null;
};
const isCurrentCaptureRecord = (record: WorkerRecord): boolean =>
state.active &&
record.captureGeneration === state.captureGeneration;
const createWorkerRecord = (
worker: InstrumentedWorker,
kind: WorkerRecord['kind']
@@ -210,11 +240,11 @@ export async function installMainCapture(
}
const record: WorkerRecord = {
cancelPostedEpochMs: null,
captureGeneration: null,
cpuFirst: null,
cpuLast: null,
elu: null,
eluStart: null,
eventLoopDelay: null,
externalPeak: 0,
finalized: false,
finalizing: null,
@@ -225,6 +255,7 @@ export async function installMainCapture(
postGcHeapUsed: null,
profileHandle: null,
profilePath: null,
requestPerformance: [],
responseEpochMs: null,
sampleBusy: false,
sampleTimer: null,
@@ -238,30 +269,13 @@ export async function installMainCapture(
return;
}
const message = incoming as JsonRecord;
const performanceCapture = readWorkerPerformance(message);
if (performanceCapture) {
record.eventLoopDelay = record.eventLoopDelay
? {
maxMs: Math.max(
record.eventLoopDelay.maxMs,
performanceCapture.eventLoopDelay.maxMs
),
p95Ms: Math.max(
record.eventLoopDelay.p95Ms,
performanceCapture.eventLoopDelay.p95Ms
),
p99Ms: Math.max(
record.eventLoopDelay.p99Ms,
performanceCapture.eventLoopDelay.p99Ms
),
}
: performanceCapture.eventLoopDelay;
if (!isCurrentCaptureRecord(record)) {
return;
}
if (record.kind === 'playlist-refresh.worker') {
if (message['type'] === 'event') {
const event = message['event'] as
| JsonRecord
| undefined;
JsonRecord | undefined;
recordTimeline({
operationId: record.operationId ?? undefined,
playlistId: record.playlistId ?? undefined,
@@ -270,7 +284,20 @@ export async function installMainCapture(
)}:${String(event?.['phase'] ?? 'none')}`,
});
} else if (message['type'] === 'response') {
record.responseEpochMs = nowEpochMs();
const responseEpochMs = nowEpochMs();
record.responseEpochMs = responseEpochMs;
record.requestPerformance.push({
identity: {
operationId: record.operationId,
operationIdUnavailableReason: null,
},
operation: null,
performanceCapture: message['performance'] ?? null,
playlistId: record.playlistId,
requestId: null,
responseEpochMs,
success: message['success'] === true,
});
recordTimeline({
operationId: record.operationId ?? undefined,
playlistId: record.playlistId ?? undefined,
@@ -285,10 +312,23 @@ export async function installMainCapture(
typeof message['requestId'] === 'string'
) {
const request = dbRequests.get(message['requestId']);
if (request) {
record.requestPerformance.push({
identity: request.identity,
operation: request.operation,
performanceCapture: message['performance'] ?? null,
playlistId: request.playlistId,
requestId: message['requestId'],
responseEpochMs: nowEpochMs(),
success: message['success'] === true,
});
}
if (request) {
dbRequests.delete(message['requestId']);
recordTimeline({
operation: request.operation,
operationId:
request.identity.operationId ?? undefined,
playlistId: request.playlistId ?? undefined,
requestId: message['requestId'],
success: message['success'] === true,
@@ -299,46 +339,6 @@ export async function installMainCapture(
});
return record;
};
const readWorkerPerformance = (
message: JsonRecord
): {
eventLoopDelay: {
maxMs: number;
p95Ms: number;
p99Ms: number;
};
} | null => {
const performanceCapture = message['performance'];
if (
typeof performanceCapture !== 'object' ||
performanceCapture === null
) {
return null;
}
const eventLoopDelay = (performanceCapture as JsonRecord)[
'eventLoopDelay'
];
if (typeof eventLoopDelay !== 'object' || eventLoopDelay === null) {
return null;
}
const value = eventLoopDelay as JsonRecord;
const maxMs = value['maxMs'];
const p95Ms = value['p95Ms'];
const p99Ms = value['p99Ms'];
if (
typeof maxMs !== 'number' ||
!Number.isFinite(maxMs) ||
typeof p95Ms !== 'number' ||
!Number.isFinite(p95Ms) ||
typeof p99Ms !== 'number' ||
!Number.isFinite(p99Ms)
) {
return null;
}
return {
eventLoopDelay: { maxMs, p95Ms, p99Ms },
};
};
const sampleWorker = async (record: WorkerRecord): Promise<void> => {
if (record.sampleBusy || record.finalized) {
return;
@@ -373,14 +373,45 @@ export async function installMainCapture(
record.sampleBusy = false;
}
};
const resetWorkerForCapture = (record: WorkerRecord): void => {
if (record.sampleTimer) {
clearInterval(record.sampleTimer);
}
record.cancelPostedEpochMs = null;
record.captureGeneration = state.captureGeneration;
record.cpuFirst = null;
record.cpuLast = null;
record.elu = null;
record.eluStart = null;
record.externalPeak = 0;
record.finalized = false;
record.finalizing = null;
record.heapPeak = 0;
record.operationId = null;
record.playlistId = null;
record.postGcHeapUsed = null;
record.profileHandle = null;
record.profilePath = null;
record.requestPerformance = [];
record.responseEpochMs = null;
record.sampleBusy = false;
record.sampleTimer = null;
record.snapshotPath = null;
record.terminatedEpochMs = null;
};
const startWorker = (record: WorkerRecord): void => {
if (!state.active || record.sampleTimer !== null) {
if (!state.active) {
return;
}
if (record.captureGeneration !== state.captureGeneration) {
resetWorkerForCapture(record);
}
if (record.sampleTimer !== null) {
return;
}
record.elu = null;
record.eluStart =
record.worker.performance?.eventLoopUtilization() ?? null;
record.eventLoopDelay = null;
void sampleWorker(record);
record.sampleTimer = setInterval(
() => void sampleWorker(record),
@@ -456,7 +487,11 @@ export async function installMainCapture(
const kind = classifyRequest(value);
if (kind) {
const record = createWorkerRecord(this, kind);
if (kind === 'playlist-refresh.worker') {
startWorker(record);
if (
isCurrentCaptureRecord(record) &&
kind === 'playlist-refresh.worker'
) {
const payload = value['payload'] as JsonRecord;
record.operationId = String(payload['operationId']);
record.playlistId = String(payload['playlistId']);
@@ -467,23 +502,47 @@ export async function installMainCapture(
type: 'playlist-request',
});
} else if (
isCurrentCaptureRecord(record) &&
typeof value['requestId'] === 'string' &&
typeof value['operation'] === 'string'
) {
const payload = value['payload'];
const payloadOperationId =
typeof payload === 'object' &&
payload !== null &&
typeof (payload as JsonRecord)[
'operationId'
] === 'string' &&
String((payload as JsonRecord)['operationId'])
.length > 0
? String(
(payload as JsonRecord)['operationId']
)
: null;
const identity =
payloadOperationId === null
? databaseRequestIdentityCapture.matchDatabaseRequest(
value
)
: {
operationId: payloadOperationId,
operationIdUnavailableReason: null,
};
dbRequests.set(value['requestId'], {
identity,
operation: value['operation'],
playlistId: readDatabasePlaylistId(value),
record,
});
recordTimeline({
operation: value['operation'],
operationId: identity.operationId ?? undefined,
playlistId:
readDatabasePlaylistId(value) ?? undefined,
requestId: value['requestId'],
type: 'db-request',
});
}
startWorker(record);
}
} else if (
value['type'] === 'cancel' &&
@@ -505,7 +564,7 @@ export async function installMainCapture(
this: InstrumentedWorker
): Promise<number> {
const record = records.get(this);
if (!record || !state.active) {
if (!record || !isCurrentCaptureRecord(record)) {
return originalTerminate.call(this);
}
const markTerminated = (code: number): number => {
@@ -627,6 +686,7 @@ export async function installMainCapture(
).length,
playlistTerminated: [...records.values()].filter(
(record) =>
isCurrentCaptureRecord(record) &&
record.kind === 'playlist-refresh.worker' &&
record.terminatedEpochMs !== null
).length,
@@ -645,7 +705,11 @@ export async function installMainCapture(
).length,
}),
start: async (options: MainCaptureStartOptions): Promise<void> => {
state.captureGeneration += 1;
state.active = true;
databaseRequestIdentityCapture.start();
dbRequests.clear();
operationWorkers.clear();
state.diagnostic = options.diagnostic;
state.outputDirectory = options.outputDirectory;
state.timeline = [];
@@ -682,7 +746,7 @@ export async function installMainCapture(
);
}
},
stop: async (): Promise<MainCaptureMetrics> => {
stop: async (): Promise<MainCaptureGenerationTransport> => {
if (state.sampleTimer) {
clearInterval(state.sampleTimer);
state.sampleTimer = null;
@@ -705,6 +769,7 @@ export async function installMainCapture(
const delay = state.eventLoopDelay;
const databaseRecords = [...records.values()].filter(
(record) =>
record.captureGeneration === state.captureGeneration &&
record.kind === 'database.worker' &&
record.sampleTimer !== null
);
@@ -756,78 +821,91 @@ export async function installMainCapture(
}
state.windowListeners = [];
state.active = false;
databaseRequestIdentityCapture.stop();
const workers = [...records.values()]
.filter(
(record) =>
record.kind === 'playlist-refresh.worker' ||
record.heapPeak > 0
record.heapPeak > 0 ||
record.requestPerformance.length > 0
)
.map((record) => ({
cancelPostedEpochMs: record.cancelPostedEpochMs,
cpuSystemMicros:
record.cpuFirst && record.cpuLast
? record.cpuLast.system - record.cpuFirst.system
: null,
cpuUserMicros:
record.cpuFirst && record.cpuLast
? record.cpuLast.user - record.cpuFirst.user
: null,
eventLoopDelay: record.eventLoopDelay,
eventLoopDelayUnavailableReason:
record.eventLoopDelay === null
? record.terminatedEpochMs !== null
? 'worker-terminated-before-profile-flush'
: 'worker-self-profile-result-unavailable'
: null,
eventLoopUtilization: record.elu,
kind: record.kind,
operationId: record.operationId,
peakExternalBytes: record.externalPeak,
peakHeapUsedBytes: record.heapPeak,
playlistId: record.playlistId,
postGcHeapUsedBytes: record.postGcHeapUsed,
profilePath: record.profilePath,
responseEpochMs: record.responseEpochMs,
snapshotPath: record.snapshotPath,
terminatedEpochMs: record.terminatedEpochMs,
captureGeneration: record.captureGeneration,
metrics: {
cancelPostedEpochMs: record.cancelPostedEpochMs,
cpuSystemMicros:
record.cpuFirst && record.cpuLast
? record.cpuLast.system -
record.cpuFirst.system
: null,
cpuUserMicros:
record.cpuFirst && record.cpuLast
? record.cpuLast.user - record.cpuFirst.user
: null,
eventLoopUtilization: record.elu,
kind: record.kind,
operationId: record.operationId,
peakExternalBytes: record.externalPeak,
peakHeapUsedBytes: record.heapPeak,
playlistId: record.playlistId,
postGcHeapUsedBytes: record.postGcHeapUsed,
profilePath: record.profilePath,
responseEpochMs: record.responseEpochMs,
snapshotPath: record.snapshotPath,
terminatedEpochMs: record.terminatedEpochMs,
},
requests: record.requestPerformance.map((request) => ({
operation: request.operation,
operationId: request.identity.operationId,
operationIdUnavailableReason:
request.identity.operationIdUnavailableReason,
performanceCapture: request.performanceCapture,
playlistId: request.playlistId,
requestId: request.requestId,
responseEpochMs: request.responseEpochMs,
success: request.success,
})),
}));
return {
cpuProfilePath: state.mainProfilePath,
cpuSystemMicros: cpu.system,
cpuUserMicros: cpu.user,
eventLoopDelay: {
maxMs: Number(delay?.max ?? 0) / 1e6,
p95Ms: Number(delay?.percentile(95) ?? 0) / 1e6,
p99Ms: Number(delay?.percentile(99) ?? 0) / 1e6,
captureGeneration: state.captureGeneration,
metrics: {
cpuProfilePath: state.mainProfilePath,
cpuSystemMicros: cpu.system,
cpuUserMicros: cpu.user,
eventLoopDelay: {
maxMs: Number(delay?.max ?? 0) / 1e6,
p95Ms: Number(delay?.percentile(95) ?? 0) / 1e6,
p99Ms: Number(delay?.percentile(99) ?? 0) / 1e6,
},
// Electron's Chromium-owned main message pump currently
// leaves Node's libuv ELU counters at zero. Preserve that
// as unavailable instead of reporting a misleading 0%.
eventLoopUtilization,
eventLoopUtilizationUnavailableReason:
eventLoopUtilization === null
? 'electron-main-embedded-event-loop'
: null,
heapSnapshotPath: state.mainSnapshotPath,
memory: {
peakHeapUsedBytes: state.mainPeakHeap,
peakRssBytes: state.mainPeakRss,
postGcHeapUsedBytes: state.postGcHeap,
postGcRssBytes: state.postGcRss,
},
rendererPeakRssBytes: state.rendererPeakRss,
responsiveEvents: state.responsiveEvents,
rssScope:
'electron-main-process-including-worker-threads-and-native-memory',
timeline: state.timeline,
unresponsiveEvents: state.unresponsiveEvents,
},
// Electron's Chromium-owned main message pump currently
// leaves Node's libuv ELU counters at zero. Preserve that
// as unavailable instead of reporting a misleading 0%.
eventLoopUtilization,
eventLoopUtilizationUnavailableReason:
eventLoopUtilization === null
? 'electron-main-embedded-event-loop'
: null,
heapSnapshotPath: state.mainSnapshotPath,
memory: {
peakHeapUsedBytes: state.mainPeakHeap,
peakRssBytes: state.mainPeakRss,
postGcHeapUsedBytes: state.postGcHeap,
postGcRssBytes: state.postGcRss,
},
rendererPeakRssBytes: state.rendererPeakRss,
responsiveEvents: state.responsiveEvents,
rssScope:
'electron-main-process-including-worker-threads-and-native-memory',
timeline: state.timeline,
unresponsiveEvents: state.unresponsiveEvents,
workers,
};
},
};
target[stateKey] = api;
}, MAIN_CAPTURE_STATE_KEY);
target[input.stateKey] = api;
}, captureStateKeys);
}
export async function startMainCapture(
@@ -861,11 +939,15 @@ export async function readMainCaptureStatus(
export async function stopMainCapture(
electronApp: ElectronApplication
): Promise<MainCaptureMetrics> {
return electronApp.evaluate(async (_electron, stateKey) => {
const target = globalThis as unknown as Record<string, unknown>;
const api = target[stateKey] as {
stop(): Promise<MainCaptureMetrics>;
};
return api.stop();
}, MAIN_CAPTURE_STATE_KEY);
const transport = await electronApp.evaluate(
async (_electron, stateKey) => {
const target = globalThis as unknown as Record<string, unknown>;
const api = target[stateKey] as {
stop(): Promise<MainCaptureGenerationTransport>;
};
return api.stop();
},
MAIN_CAPTURE_STATE_KEY
);
return selectMainCaptureGeneration(transport);
}
@@ -0,0 +1,259 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based configuration contract tests. */
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { join } from 'node:path';
interface TargetConfiguration {
configurations?: Record<string, Record<string, unknown>>;
dependsOn?: unknown;
executor?: unknown;
options?: Record<string, unknown>;
}
interface ProjectConfiguration {
targets: Record<string, TargetConfiguration>;
}
interface NxGraphTask {
outputs?: string[];
target: {
project: string;
target: string;
};
}
interface NxGraph {
tasks: {
tasks: Record<string, NxGraphTask>;
};
}
const workspaceRoot = fileURLToPath(new URL('../../../../', import.meta.url));
function readProject(relativePath: string): ProjectConfiguration {
return JSON.parse(
readFileSync(join(workspaceRoot, relativePath), 'utf8')
) as ProjectConfiguration;
}
function readResolvedWebBuildTask(): NxGraphTask | undefined {
const environment = {
...process.env,
FORCE_COLOR: '0',
NX_DAEMON: 'false',
};
delete environment['NO_COLOR'];
const graph = JSON.parse(
execFileSync(
process.execPath,
[
join(workspaceRoot, 'node_modules/nx/dist/bin/nx.js'),
'run',
'web:build',
'--graph=stdout',
],
{
cwd: workspaceRoot,
encoding: 'utf8',
env: environment,
}
)
) as NxGraph;
return Object.values(graph.tasks.tasks).find(
(task) =>
task.target.project === 'web' && task.target.target === 'build'
);
}
const e2eProject = readProject('apps/electron-backend-e2e/project.json');
const electronProject = readProject('apps/electron-backend/project.json');
const remoteControlProject = readProject(
'apps/remote-control-web/project.json'
);
const webProject = readProject('apps/web/project.json');
test('the performance harness target runs only Node performance specs', () => {
const target = e2eProject.targets['test-performance-harness'];
assert.ok(
target,
'electron-backend-e2e must define test-performance-harness'
);
assert.equal(target.executor, 'nx:run-commands');
assert.equal(target.options?.['cwd'], 'apps/electron-backend-e2e');
assert.equal(
target.options?.['command'],
'pnpm exec tsx --test src/performance/*.spec.ts'
);
});
test('the web performance build keeps production renderer behavior with profiling source maps', () => {
const build = webProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'web:build must define production');
assert.ok(performance, 'web:build must define electron-performance');
assert.equal(performance['baseHref'], './');
assert.equal(performance['serviceWorker'], false);
assert.deepEqual(performance['optimization'], production['optimization']);
assert.equal(performance['outputHashing'], production['outputHashing']);
assert.deepEqual(
performance['fileReplacements'],
production['fileReplacements']
);
assert.equal(performance['sourceMap'], true);
});
test('the resolved web build cache output is the renderer directory', () => {
const task = readResolvedWebBuildTask();
assert.ok(task, 'the Nx graph must include web:build');
assert.deepEqual(task.outputs, ['dist/apps/web']);
});
test('the remote-control performance build keeps optimized hashed output with profiling source maps', () => {
const build = remoteControlProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'remote-control-web:build must define production');
assert.ok(
performance,
'remote-control-web:build must define electron-performance'
);
assert.equal(performance['optimization'], true);
assert.equal(performance['outputHashing'], production['outputHashing']);
assert.equal(performance['sourceMap'], true);
});
test('the Electron performance build keeps production main-process behavior with profiling source maps', () => {
const build = electronProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'electron-backend:build must define production');
assert.ok(
performance,
'electron-backend:build must define electron-performance'
);
assert.equal(performance['optimization'], production['optimization']);
assert.equal(performance['inspect'], production['inspect']);
assert.deepEqual(
performance['fileReplacements'],
production['fileReplacements']
);
assert.equal(performance['sourceMap'], true);
});
test('the regular Electron build keeps its existing renderer dependency contract', () => {
const build = electronProject.targets['build'];
const dependencies = build.dependsOn as Array<
Record<string, unknown> | string
>;
const rendererBuild = dependencies.find(
(dependency): dependency is Record<string, unknown> =>
typeof dependency === 'object' &&
dependency !== null &&
dependency['target'] === 'build'
);
assert.deepEqual(rendererBuild, {
projects: ['web', 'remote-control-web'],
target: 'build',
});
});
test('the web performance wrapper selects the profiling configuration', () => {
const target = webProject.targets['build-performance'];
assert.ok(target, 'web must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'pnpm nx run web:build:electron-performance'
);
});
test('the remote-control performance wrapper selects the profiling configuration', () => {
const target = remoteControlProject.targets['build-performance'];
assert.ok(target, 'remote-control-web must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'pnpm nx run remote-control-web:build:electron-performance'
);
});
test('the Electron performance wrapper owns build dependencies before its profiled build', () => {
const target = electronProject.targets['build-performance'];
assert.ok(target, 'electron-backend must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.deepEqual(target.dependsOn, [
'electron-backend:build-worker-performance',
'electron-backend:build-embedded-mpv',
{
projects: ['web', 'remote-control-web'],
target: 'build-performance',
},
]);
assert.equal(
target.options?.['command'],
'pnpm nx run electron-backend:build:electron-performance --excludeTaskDependencies'
);
});
test('the Electron performance wrapper builds optimized source-mapped workers', () => {
const target = electronProject.targets['build-worker-performance'];
const source = readFileSync(
join(workspaceRoot, 'apps/electron-backend/build-worker.js'),
'utf8'
);
assert.ok(target, 'electron-backend must define build-worker-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'node apps/electron-backend/build-worker.js --performance'
);
assert.match(
source,
/const isPerformance = process\.argv\.includes\('--performance'\)/
);
assert.match(source, /minify: isProduction \|\| isPerformance/);
assert.match(source, /sourcemap: isPerformance \|\| !isProduction/);
});
test('the cancellation benchmark uses the production-equivalent performance build', () => {
const target = e2eProject.targets['benchmark-m3u-refresh-cancellation'];
assert.deepEqual(target.dependsOn, ['electron-backend:build-performance']);
});
test('the cancellation benchmark command is pinned to its Playwright test file', () => {
const target = e2eProject.targets['benchmark-m3u-refresh-cancellation'];
assert.equal(
target.options?.['command'],
'pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-refresh-cancellation.performance.ts'
);
});
test('the cancellation benchmark enables preload performance capture', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation.benchmark.ts'
),
'utf8'
);
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
});
@@ -0,0 +1,172 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import test from 'node:test';
import type { WorkerRequestPerformanceMetrics } from './m3u-refresh-cancellation-contract';
import { normalizeWorkerRequestPerformanceOutcome } from './worker-request-performance';
const validCapture = {
eventLoopDelay: { maxMs: 6, p95Ms: 5, p99Ms: 5.5 },
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: 0.5,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 1_050,
invalidReason: null,
requestReceivedEpochMs: 1_000,
threadCpuSystemMicros: 50,
threadCpuUnavailableReason: null,
threadCpuUserMicros: 100,
workEndedEpochMs: 1_040,
workStartedEpochMs: 1_010,
} as const;
function normalize(performanceCapture: unknown) {
return normalizeWorkerRequestPerformanceOutcome({
operation: 'DB_GET_APP_PLAYLIST',
operationId: 'operation-42',
operationIdUnavailableReason: null,
performanceCapture,
playlistId: 'playlist-1',
requestId: 'request-1',
responseEpochMs: 1_060,
success: true,
});
}
function assertClosedCapture(
capture: WorkerRequestPerformanceMetrics,
reason: string
): void {
assert.equal(capture.performanceCaptureUnavailableReason, reason);
assert.equal(capture.operationId, 'operation-42');
for (const value of [
capture.eventLoopDelay,
capture.eventLoopDelayUnavailableReason,
capture.eventLoopUtilization,
capture.eventLoopUtilizationUnavailableReason,
capture.histogramFlushedEpochMs,
capture.invalidReason,
capture.requestReceivedEpochMs,
capture.threadCpuSystemMicros,
capture.threadCpuUnavailableReason,
capture.threadCpuUserMicros,
capture.workEndedEpochMs,
capture.workStartedEpochMs,
]) {
assert.equal(value, null);
}
}
test('absent captures are missing while present non-record captures are invalid', () => {
for (const missing of [undefined, null]) {
assertClosedCapture(
normalize(missing),
'worker-performance-capture-missing'
);
}
for (const malformed of [42, [], 'capture']) {
assertClosedCapture(
normalize(malformed),
'worker-performance-capture-invalid'
);
}
});
test('malformed nested fields and cross-field invariants fail the whole capture closed', () => {
const malformed = [
{ ...validCapture, eventLoopDelay: [] },
{
...validCapture,
eventLoopDelay: { maxMs: 6, p95Ms: 'bad', p99Ms: 5.5 },
},
{
...validCapture,
eventLoopDelay: { maxMs: 5, p95Ms: 6, p99Ms: 5.5 },
},
{ ...validCapture, eventLoopDelayUnavailableReason: 42 },
{
...validCapture,
eventLoopDelay: null,
eventLoopDelayUnavailableReason: null,
histogramFlushedEpochMs: null,
},
{
...validCapture,
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
'event-loop-delay-capture-unavailable',
},
{ ...validCapture, eventLoopUtilization: 1.01 },
{
...validCapture,
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason: null,
},
{ ...validCapture, threadCpuSystemMicros: null },
{
...validCapture,
threadCpuSystemMicros: null,
threadCpuUnavailableReason: null,
threadCpuUserMicros: null,
},
{ ...validCapture, threadCpuUnavailableReason: 'unavailable' },
{ ...validCapture, histogramFlushedEpochMs: null },
{
...validCapture,
eventLoopDelay: null,
eventLoopDelayUnavailableReason: 'event-loop-delay-invalid',
histogramFlushedEpochMs: null,
},
{ ...validCapture, histogramFlushedEpochMs: 1_039 },
{ ...validCapture, workStartedEpochMs: 999 },
{
...validCapture,
invalidReason: 'overlapping-database-worker-requests',
},
{ ...validCapture, threadCpuUserMicros: undefined },
];
for (const performanceCapture of malformed) {
assertClosedCapture(
normalize(performanceCapture),
'worker-performance-capture-invalid'
);
}
});
test('coherent metric-unavailable and invalid worker results remain valid raw captures', () => {
const unavailable = {
...validCapture,
eventLoopDelay: null,
eventLoopDelayUnavailableReason: 'event-loop-delay-capture-unavailable',
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
'event-loop-utilization-unavailable',
histogramFlushedEpochMs: null,
threadCpuSystemMicros: null,
threadCpuUnavailableReason: 'thread-cpu-usage-unavailable',
threadCpuUserMicros: null,
};
for (const capture of [
unavailable,
{
...validCapture,
eventLoopDelay: null,
eventLoopDelayUnavailableReason: 'event-loop-delay-invalid',
},
]) {
const accepted = normalize(capture);
assert.equal(accepted.performanceCaptureUnavailableReason, null);
assert.equal(accepted.requestReceivedEpochMs, 1_000);
}
const invalidReason = 'overlapping-database-worker-requests';
const legitimateInvalid = normalize({
...unavailable,
eventLoopDelayUnavailableReason: invalidReason,
eventLoopUtilizationUnavailableReason: invalidReason,
invalidReason,
threadCpuUnavailableReason: invalidReason,
});
assert.equal(legitimateInvalid.performanceCaptureUnavailableReason, null);
assert.equal(legitimateInvalid.invalidReason, invalidReason);
});
@@ -0,0 +1,302 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
type CancellationBenchmarkManifest,
type CancellationIterationResult,
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
type WorkerCaptureMetrics,
type WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
normalizeWorkerRequestPerformanceOutcome,
selectMainCaptureGeneration,
} from './worker-request-performance';
import { createCancellationBenchmarkSummary } from './m3u-refresh-cancellation-report';
type RequestPerformanceFixture = WorkerRequestPerformanceMetrics;
function requestPerformance(
requestId: string,
p95Ms: number,
threadCpuUserMicros: number
): RequestPerformanceFixture {
return normalizeWorkerRequestPerformanceOutcome(
requestTransport(
{
eventLoopDelay: {
maxMs: p95Ms + 1,
p95Ms,
p99Ms: p95Ms + 0.5,
},
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: p95Ms / 100,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 1_050 + p95Ms,
invalidReason: null,
requestReceivedEpochMs: 1_000,
threadCpuSystemMicros: threadCpuUserMicros / 2,
threadCpuUnavailableReason: null,
threadCpuUserMicros,
workEndedEpochMs: 1_040,
workStartedEpochMs: 1_010,
},
requestId
)
);
}
function requestTransport(
performanceCapture: unknown,
requestId = 'request-1'
) {
return {
operation: 'DB_GET_APP_PLAYLIST',
operationId: 'operation-42',
operationIdUnavailableReason: null,
performanceCapture,
playlistId: 'playlist-1',
requestId,
responseEpochMs: 1_060,
success: true,
} as const;
}
function assertClosedCapture(
capture: WorkerRequestPerformanceMetrics,
reason: string
): void {
assert.equal(capture.performanceCaptureUnavailableReason, reason);
assert.equal(capture.operationId, 'operation-42');
for (const value of [
capture.eventLoopDelay,
capture.eventLoopDelayUnavailableReason,
capture.eventLoopUtilization,
capture.eventLoopUtilizationUnavailableReason,
capture.histogramFlushedEpochMs,
capture.invalidReason,
capture.requestReceivedEpochMs,
capture.threadCpuSystemMicros,
capture.threadCpuUnavailableReason,
capture.threadCpuUserMicros,
capture.workEndedEpochMs,
capture.workStartedEpochMs,
]) {
assert.equal(value, null);
}
}
function databaseWorker(
requests: readonly RequestPerformanceFixture[]
): WorkerCaptureMetrics {
return {
kind: PERFORMANCE_WORKER_KIND.DATABASE,
operationId: null,
peakExternalBytes: 0,
peakHeapUsedBytes: 0,
playlistId: null,
postGcHeapUsedBytes: null,
requests,
responseEpochMs: null,
terminatedEpochMs: null,
} as WorkerCaptureMetrics;
}
function playlistWorker(
request: RequestPerformanceFixture,
operationId: string,
terminatedEpochMs: number
): WorkerCaptureMetrics {
return {
...databaseWorker([request]),
kind: PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
operationId,
playlistId: request.playlistId,
responseEpochMs: request.responseEpochMs,
terminatedEpochMs,
};
}
function measuredIteration(
workers: readonly WorkerCaptureMetrics[]
): CancellationIterationResult {
return {
cancellationEffectObserved: true,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: {
eventLoopDelay: { maxMs: 0, p95Ms: 0, p99Ms: 0 },
eventLoopUtilization: null,
memory: {
peakHeapUsedBytes: 0,
peakRssBytes: 0,
postGcHeapUsedBytes: null,
postGcRssBytes: null,
},
rendererPeakRssBytes: 0,
responsiveEvents: 0,
unresponsiveEvents: 0,
workers,
} as CancellationIterationResult['main'],
phases: {} as CancellationIterationResult['phases'],
renderer: {
peakHeapUsedBytes: 0,
postGcHeapUsedBytes: null,
probe: {
frameGapsMs: [],
heartbeatDelaysMs: [],
longTasksMs: [],
},
} as CancellationIterationResult['renderer'],
runId: 'measured-1',
};
}
test('summary distributions use every request capture instead of a worker-level percentile maximum', () => {
const requests = [
requestPerformance('request-1', 5, 100),
requestPerformance('request-2', 15, 300),
];
const summary = createCancellationBenchmarkSummary(
{} as CancellationBenchmarkManifest,
[measuredIteration([databaseWorker(requests)])]
);
const measured = summary.measured as unknown as Record<
string,
{ count: number; max: number | null; min: number | null }
>;
assert.deepEqual(
summary.measured.databaseWorkerRequestEventLoopDelayP95Ms,
{
count: 2,
max: 15,
mean: 10,
median: 10,
min: 5,
p95: 14.5,
p99: 14.9,
}
);
assert.deepEqual(measured['databaseWorkerRequestThreadCpuUserMicros'], {
count: 2,
max: 300,
mean: 200,
median: 200,
min: 100,
p95: 290,
p99: 298,
});
assert.equal(
summary.iterations[0]?.main.workers[0]?.requests[0]?.operationId,
'operation-42'
);
assert.equal(
'databaseWorkerEventLoopDelayP95Ms' in summary.measured,
false,
'request-percentile distributions must not look like an operation-wide percentile'
);
});
test('main capture retains raw request identity, timestamps, metrics, and reasons', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
assert.match(source, /requestPerformance: \[\]/);
assert.match(source, /record\.requestPerformance\.length > 0/);
assert.match(source, /record\.requestPerformance\.map/);
assert.match(source, /operationId: request\.identity\.operationId/);
assert.match(source, /identity: request\.identity/);
assert.match(source, /performanceCapture:\s+message\['performance'\]/);
assert.match(source, /captureGeneration: state\.captureGeneration/);
assert.doesNotMatch(
source,
/record\.eventLoopDelay = record\.eventLoopDelay/
);
});
test('capture-generation selection excludes a pre-start seed worker', () => {
const seed = playlistWorker(
requestPerformance('seed-request', 999, 999),
'seed-operation',
1_100
);
const measured = playlistWorker(
{
...requestPerformance('measured-request', 8, 80),
success: false,
},
'measured-cancel-operation',
2_100
);
const mainMetrics = measuredIteration([]).main;
const toTransportWorker = (
captureGeneration: number | null,
worker: WorkerCaptureMetrics
) => ({
captureGeneration,
metrics: worker,
requests: worker.requests.map((request) => ({
...requestTransport(request, request.requestId ?? undefined),
success: request.success,
})),
});
const selected = selectMainCaptureGeneration({
captureGeneration: 2,
metrics: mainMetrics,
workers: [
toTransportWorker(null, seed),
toTransportWorker(2, measured),
],
});
assert.equal(selected.workers.length, 1);
assert.equal(
selected.workers[0]?.requests[0]?.requestId,
'measured-request'
);
assert.equal(selected.workers[0]?.operationId, 'measured-cancel-operation');
assert.equal(selected.workers[0]?.terminatedEpochMs, 2_100);
});
test('raw outcomes retain missing and malformed captures while summaries exclude their null metrics', () => {
const valid = normalizeWorkerRequestPerformanceOutcome(
requestTransport(requestPerformance('request-1', 5, 100))
);
const missing = normalizeWorkerRequestPerformanceOutcome(
requestTransport(null, 'request-2')
);
const malformed = normalizeWorkerRequestPerformanceOutcome(
requestTransport(
{
requestReceivedEpochMs: 'invalid',
workEndedEpochMs: 1_080,
workStartedEpochMs: 1_075,
},
'request-3'
)
);
const worker = databaseWorker([valid, missing, malformed]);
const summary = createCancellationBenchmarkSummary(
{} as CancellationBenchmarkManifest,
[measuredIteration([worker])]
);
assert.equal(worker.requests.length, 3);
assertClosedCapture(
worker.requests[1] as WorkerRequestPerformanceMetrics,
'worker-performance-capture-missing'
);
assertClosedCapture(
worker.requests[2] as WorkerRequestPerformanceMetrics,
'worker-performance-capture-invalid'
);
assert.equal(
summary.measured.databaseWorkerRequestEventLoopDelayP95Ms.count,
1
);
assert.equal(
summary.measured.databaseWorkerRequestThreadCpuUserMicros.count,
1
);
});
@@ -0,0 +1,302 @@
import type {
EventLoopDelayMetrics,
MainCaptureMetrics,
WorkerCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
export interface WorkerRequestPerformanceOutcomeTransport {
readonly operation: string | null;
readonly operationId: string | null;
readonly operationIdUnavailableReason: string | null;
readonly performanceCapture: unknown;
readonly playlistId: string | null;
readonly requestId: string | null;
readonly responseEpochMs: number;
readonly success: boolean;
}
export interface MainCaptureGenerationTransport {
readonly captureGeneration: number;
readonly metrics: Omit<MainCaptureMetrics, 'workers'>;
readonly workers: readonly {
readonly captureGeneration: number | null;
readonly metrics: Omit<
WorkerCaptureMetrics,
'eventLoopDelay' | 'eventLoopDelayUnavailableReason' | 'requests'
>;
readonly requests: readonly WorkerRequestPerformanceOutcomeTransport[];
}[];
}
type ParsedWorkerPerformanceCapture = Pick<
WorkerRequestPerformanceMetrics,
| 'eventLoopDelay'
| 'eventLoopDelayUnavailableReason'
| 'eventLoopUtilization'
| 'eventLoopUtilizationUnavailableReason'
| 'histogramFlushedEpochMs'
| 'invalidReason'
| 'requestReceivedEpochMs'
| 'threadCpuSystemMicros'
| 'threadCpuUnavailableReason'
| 'threadCpuUserMicros'
| 'workEndedEpochMs'
| 'workStartedEpochMs'
>;
const INVALID_REASONS = new Set(['overlapping-database-worker-requests']);
const EVENT_LOOP_DELAY_REASONS = new Set([
...INVALID_REASONS,
'event-loop-delay-arm-timeout',
'event-loop-delay-capture-unavailable',
'event-loop-delay-flush-timeout',
'event-loop-delay-invalid',
]);
const EVENT_LOOP_UTILIZATION_REASONS = new Set([
...INVALID_REASONS,
'event-loop-utilization-unavailable',
]);
const THREAD_CPU_REASONS = new Set([
...INVALID_REASONS,
'thread-cpu-usage-invalid',
'thread-cpu-usage-unavailable',
]);
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 isNullableFiniteNonNegativeNumber(
value: unknown
): value is number | null {
return value === null || isFiniteNonNegativeNumber(value);
}
function isNullableReason(
value: unknown,
allowed: ReadonlySet<string>
): value is string | null {
return value === null || (typeof value === 'string' && allowed.has(value));
}
function parseEventLoopDelay(
input: unknown
): EventLoopDelayMetrics | null | undefined {
if (input === null) {
return null;
}
if (!isRecord(input)) {
return undefined;
}
const maxMs = input['maxMs'];
const p95Ms = input['p95Ms'];
const p99Ms = input['p99Ms'];
if (
!isFiniteNonNegativeNumber(maxMs) ||
!isFiniteNonNegativeNumber(p95Ms) ||
!isFiniteNonNegativeNumber(p99Ms) ||
p95Ms > p99Ms ||
p99Ms > maxMs
) {
return undefined;
}
return { maxMs, p95Ms, p99Ms };
}
function parseWorkerPerformanceCapture(
input: unknown
): ParsedWorkerPerformanceCapture | null {
if (!isRecord(input)) {
return null;
}
const eventLoopDelay = parseEventLoopDelay(input['eventLoopDelay']);
const eventLoopDelayUnavailableReason =
input['eventLoopDelayUnavailableReason'];
const eventLoopUtilization = input['eventLoopUtilization'];
const eventLoopUtilizationUnavailableReason =
input['eventLoopUtilizationUnavailableReason'];
const histogramFlushedEpochMs = input['histogramFlushedEpochMs'];
const invalidReason = input['invalidReason'];
const requestReceivedEpochMs = input['requestReceivedEpochMs'];
const threadCpuSystemMicros = input['threadCpuSystemMicros'];
const threadCpuUnavailableReason = input['threadCpuUnavailableReason'];
const threadCpuUserMicros = input['threadCpuUserMicros'];
const workEndedEpochMs = input['workEndedEpochMs'];
const workStartedEpochMs = input['workStartedEpochMs'];
if (
eventLoopDelay === undefined ||
!isNullableReason(
eventLoopDelayUnavailableReason,
EVENT_LOOP_DELAY_REASONS
) ||
!isNullableFiniteNonNegativeNumber(eventLoopUtilization) ||
(eventLoopUtilization !== null && eventLoopUtilization > 1) ||
!isNullableReason(
eventLoopUtilizationUnavailableReason,
EVENT_LOOP_UTILIZATION_REASONS
) ||
!isNullableFiniteNonNegativeNumber(histogramFlushedEpochMs) ||
!isNullableReason(invalidReason, INVALID_REASONS) ||
!isFiniteNonNegativeNumber(requestReceivedEpochMs) ||
!isNullableFiniteNonNegativeNumber(threadCpuSystemMicros) ||
!isNullableReason(threadCpuUnavailableReason, THREAD_CPU_REASONS) ||
!isNullableFiniteNonNegativeNumber(threadCpuUserMicros) ||
!isFiniteNonNegativeNumber(workEndedEpochMs) ||
!isFiniteNonNegativeNumber(workStartedEpochMs)
) {
return null;
}
const timestampsAreOrdered =
requestReceivedEpochMs <= workStartedEpochMs &&
workStartedEpochMs <= workEndedEpochMs &&
(histogramFlushedEpochMs === null ||
workEndedEpochMs <= histogramFlushedEpochMs);
const eventLoopDelayIsCoherent =
eventLoopDelay === null
? eventLoopDelayUnavailableReason !== null &&
(eventLoopDelayUnavailableReason === 'event-loop-delay-invalid'
? histogramFlushedEpochMs !== null
: histogramFlushedEpochMs === null)
: eventLoopDelayUnavailableReason === null &&
histogramFlushedEpochMs !== null;
const eventLoopUtilizationIsCoherent =
eventLoopUtilization === null
? eventLoopUtilizationUnavailableReason !== null
: eventLoopUtilizationUnavailableReason === null;
const threadCpuIsCoherent =
threadCpuSystemMicros === null && threadCpuUserMicros === null
? threadCpuUnavailableReason !== null
: threadCpuSystemMicros !== null &&
threadCpuUserMicros !== null &&
threadCpuUnavailableReason === null;
const invalidCaptureIsCoherent =
invalidReason === null ||
(eventLoopDelay === null &&
eventLoopDelayUnavailableReason === invalidReason &&
eventLoopUtilization === null &&
eventLoopUtilizationUnavailableReason === invalidReason &&
histogramFlushedEpochMs === null &&
threadCpuSystemMicros === null &&
threadCpuUnavailableReason === invalidReason &&
threadCpuUserMicros === null);
if (
!timestampsAreOrdered ||
!eventLoopDelayIsCoherent ||
!eventLoopUtilizationIsCoherent ||
!threadCpuIsCoherent ||
!invalidCaptureIsCoherent
) {
return null;
}
return {
eventLoopDelay,
eventLoopDelayUnavailableReason,
eventLoopUtilization,
eventLoopUtilizationUnavailableReason,
histogramFlushedEpochMs,
invalidReason,
requestReceivedEpochMs,
threadCpuSystemMicros,
threadCpuUnavailableReason,
threadCpuUserMicros,
workEndedEpochMs,
workStartedEpochMs,
};
}
export function normalizeWorkerRequestPerformanceOutcome(
input: WorkerRequestPerformanceOutcomeTransport
): WorkerRequestPerformanceMetrics {
const identity = {
operation: input.operation,
operationId: input.operationId,
operationIdUnavailableReason: input.operationIdUnavailableReason,
playlistId: input.playlistId,
requestId: input.requestId,
responseEpochMs: input.responseEpochMs,
success: input.success,
};
const unavailable = (
reason:
| 'worker-performance-capture-invalid'
| 'worker-performance-capture-missing'
): WorkerRequestPerformanceMetrics => ({
eventLoopDelay: null,
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: null,
invalidReason: null,
...identity,
performanceCaptureUnavailableReason: reason,
requestReceivedEpochMs: null,
threadCpuSystemMicros: null,
threadCpuUnavailableReason: null,
threadCpuUserMicros: null,
workEndedEpochMs: null,
workStartedEpochMs: null,
});
if (
input.performanceCapture === undefined ||
input.performanceCapture === null
) {
return unavailable('worker-performance-capture-missing');
}
const performanceCapture = parseWorkerPerformanceCapture(
input.performanceCapture
);
if (!performanceCapture) {
return unavailable('worker-performance-capture-invalid');
}
return {
...performanceCapture,
...identity,
performanceCaptureUnavailableReason: null,
};
}
export function selectMainCaptureGeneration(
input: MainCaptureGenerationTransport
): MainCaptureMetrics {
const workers = input.workers
.filter(
(worker) => worker.captureGeneration === input.captureGeneration
)
.map((worker): WorkerCaptureMetrics => {
const requests = worker.requests.map(
normalizeWorkerRequestPerformanceOutcome
);
const onlyRequest =
requests.length === 1 ? (requests[0] ?? null) : null;
return {
...worker.metrics,
eventLoopDelay: onlyRequest?.eventLoopDelay ?? null,
eventLoopDelayUnavailableReason: onlyRequest
? (onlyRequest.eventLoopDelayUnavailableReason ??
onlyRequest.performanceCaptureUnavailableReason ??
'worker-self-profile-result-unavailable')
: requests.length > 1
? 'multiple-request-scoped-captures'
: worker.metrics.terminatedEpochMs !== null
? 'worker-terminated-before-profile-flush'
: 'worker-self-profile-result-unavailable',
requests,
};
});
return {
...input.metrics,
workers,
};
}
+9 -3
View File
@@ -18,6 +18,12 @@ const nativeModules = [
];
const isProduction = process.env.NODE_ENV === 'production';
const isPerformance = process.argv.includes('--performance');
const buildMode = isPerformance
? 'performance'
: isProduction
? 'production'
: 'development';
async function buildWorker() {
try {
@@ -59,7 +65,7 @@ async function buildWorker() {
for (const worker of workers) {
console.log(
`Building ${worker.label} with esbuild (${isProduction ? 'production' : 'development'})...`
`Building ${worker.label} with esbuild (${buildMode})...`
);
await esbuild.build({
@@ -74,8 +80,8 @@ async function buildWorker() {
...nodeBuiltins,
...nativeModules,
],
sourcemap: !isProduction,
minify: isProduction,
sourcemap: isPerformance || !isProduction,
minify: isProduction || isPerformance,
alias: {
'@iptvnator/shared/interfaces': path.join(
__dirname,
+33
View File
@@ -12,6 +12,13 @@
"command": "node apps/electron-backend/build-worker.js"
}
},
"build-worker-performance": {
"executor": "nx:run-commands",
"outputs": ["{workspaceRoot}/dist/apps/electron-backend/workers"],
"options": {
"command": "node apps/electron-backend/build-worker.js --performance"
}
},
"build-embedded-mpv": {
"executor": "nx:run-commands",
"cache": false,
@@ -81,9 +88,35 @@
"with": "apps/electron-backend/src/environments/environment.prod.ts"
}
]
},
"electron-performance": {
"optimization": true,
"extractLicenses": true,
"inspect": false,
"sourceMap": true,
"fileReplacements": [
{
"replace": "apps/electron-backend/src/environments/environment.ts",
"with": "apps/electron-backend/src/environments/environment.prod.ts"
}
]
}
}
},
"build-performance": {
"dependsOn": [
"electron-backend:build-worker-performance",
"electron-backend:build-embedded-mpv",
{
"projects": ["web", "remote-control-web"],
"target": "build-performance"
}
],
"executor": "nx:run-commands",
"options": {
"command": "pnpm nx run electron-backend:build:electron-performance --excludeTaskDependencies"
}
},
"build-e2e": {
"dependsOn": [
"electron-backend:build-worker",
@@ -0,0 +1,343 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type Playlist,
type PreloadPerformanceMarker,
} from '@iptvnator/shared/interfaces';
import {
createPlaylist,
createPreloadPerformanceHarness,
createRefreshPayload,
expectFixedMarkers,
PRELOAD_ENVIRONMENT,
type PreloadPerformanceHarness,
} from './main.preload.performance.test-helpers';
describe('main preload performance marker contract', () => {
let harness: PreloadPerformanceHarness;
beforeEach(() => {
harness = createPreloadPerformanceHarness();
});
afterEach(() => {
harness.cleanup();
});
async function loadPerformancePreload(): Promise<void> {
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
});
}
it('preserves calls while emitting ordered, correlated, fixed, redacted markers', async () => {
const secrets = {
favorite: 'sentinel-favorite-secret',
filePath: '/sentinel/private/playlist.m3u',
host: 'sentinel.private.example',
item: 'sentinel-item-secret',
result: 'sentinel-result-secret',
title: 'sentinel-title-secret',
url: 'https://sentinel.example/private.m3u?token=secret',
};
const payload = createRefreshPayload({
filePath: secrets.filePath,
title: secrets.title,
trustedInsecureTlsHosts: [secrets.host],
url: secrets.url,
});
const refreshedPlaylist = createPlaylist(payload.playlistId, {
favorites: [secrets.favorite],
items: [{ name: secrets.item }],
title: secrets.title,
url: secrets.url,
});
const storedPlaylist = createPlaylist(payload.playlistId, {
filePath: secrets.filePath,
title: secrets.title,
});
const upsertPlaylist = createPlaylist(payload.playlistId, {
favorites: [secrets.favorite],
items: [{ name: secrets.item }],
title: secrets.title,
});
const upsertResult = {
secretResult: secrets.result,
success: true,
};
await loadPerformancePreload();
harness.ipcRenderer.invoke
.mockResolvedValueOnce(refreshedPlaylist)
.mockResolvedValueOnce(storedPlaylist)
.mockResolvedValueOnce(upsertResult);
const api = harness.getApi();
await expect(api.refreshPlaylist(payload)).resolves.toBe(
refreshedPlaylist
);
await expect(
api.dbGetAppPlaylist(payload.playlistId, payload.operationId)
).resolves.toBe(storedPlaylist);
await expect(
api.dbUpsertAppPlaylist(upsertPlaylist, payload.operationId)
).resolves.toBe(upsertResult);
expect(harness.ipcRenderer.invoke.mock.calls).toEqual([
['PLAYLIST:REFRESH', payload],
['DB_GET_APP_PLAYLIST', payload.playlistId],
['DB_UPSERT_APP_PLAYLIST', upsertPlaylist],
]);
const markers = harness.getMarkers();
expect(markers).toHaveLength(6);
expect(
markers.map(({ ipcCallId, method, phase }) => [
ipcCallId,
method,
phase,
])
).toEqual([
[1, PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST, 'start'],
[1, PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST, 'success'],
[2, PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST, 'start'],
[2, PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST, 'success'],
[3, PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST, 'start'],
[3, PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST, 'success'],
]);
expect(markers.map(({ correlationState }) => correlationState)).toEqual(
[
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
]
);
expect(markers.map(({ operationId }) => operationId)).toEqual(
Array(6).fill(payload.operationId)
);
expect(markers.map(({ playlistId }) => playlistId)).toEqual(
Array(6).fill(payload.playlistId)
);
expect(markers.map(({ invalidReason }) => invalidReason)).toEqual(
Array(6).fill(null)
);
expectFixedMarkers(markers);
const markerSendOrders = harness.ipcRenderer.send.mock.calls.flatMap(
([channel], index) =>
channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL
? [harness.ipcRenderer.send.mock.invocationCallOrder[index]]
: []
);
const invokeOrders =
harness.ipcRenderer.invoke.mock.invocationCallOrder;
for (const [index, invokeOrder] of invokeOrders.entries()) {
expect(markerSendOrders[index * 2]).toBeLessThan(invokeOrder);
}
const serializedMarkers = JSON.stringify(markers);
for (const secret of Object.values(secrets)) {
expect(serializedMarkers).not.toContain(secret);
}
});
it('preserves rejection identity and emits a redacted error', async () => {
const payload = createRefreshPayload({
title: 'sentinel-rejection-title',
url: 'https://sentinel.example/rejected.m3u',
});
const rejection = new Error('sentinel-rejection-error');
await loadPerformancePreload();
harness.ipcRenderer.invoke.mockRejectedValueOnce(rejection);
await expect(harness.getApi().refreshPlaylist(payload)).rejects.toBe(
rejection
);
const markers = harness.getMarkers();
expect(markerPhases(markers)).toEqual([
[1, 'start'],
[1, 'error'],
]);
expect(markers[1]).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR,
operationId: payload.operationId,
playlistId: payload.playlistId,
});
expect(JSON.stringify(markers)).not.toContain(rejection.message);
expect(JSON.stringify(markers)).not.toContain(payload.title);
expect(JSON.stringify(markers)).not.toContain(payload.url);
});
it('preserves synchronous throw identity and emits a redacted error', async () => {
const synchronousError = new Error('sentinel-synchronous-error');
const payload = createRefreshPayload();
await loadPerformancePreload();
harness.ipcRenderer.invoke.mockImplementationOnce(() => {
throw synchronousError;
});
let caughtError: unknown;
try {
harness.getApi().refreshPlaylist(payload);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(synchronousError);
const markers = harness.getMarkers();
expect(markerPhases(markers)).toEqual([
[1, 'start'],
[1, 'error'],
]);
expect(markers[1]).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR,
});
expect(JSON.stringify(markers)).not.toContain(synchronousError.message);
});
it('closes a structured refresh cancellation with a fixed reason', async () => {
const payload = createRefreshPayload();
const cancelledResult = {
operationId: payload.operationId,
type: 'playlist-refresh-cancelled',
} as const;
await loadPerformancePreload();
harness.ipcRenderer.invoke
.mockResolvedValueOnce(cancelledResult)
.mockResolvedValueOnce(null);
const api = harness.getApi();
await expect(api.refreshPlaylist(payload)).resolves.toBe(
cancelledResult
);
await api.dbGetAppPlaylist(payload.playlistId);
const markers = harness.getMarkers();
expect(markers[1]).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.REFRESH_CANCELLED,
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: payload.operationId,
phase: 'success',
playlistId: payload.playlistId,
});
expect(markers[2]).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
});
});
it('uses the exact upsert _id and keeps one malformed reason in both phases', async () => {
const malformedPlaylist = {
...createPlaylist('placeholder'),
_id: {
secret: 'sentinel-malformed-playlist-id',
},
} as unknown as Playlist;
const result = { success: true };
await loadPerformancePreload();
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
await expect(
harness.getApi().dbUpsertAppPlaylist(malformedPlaylist)
).resolves.toBe(result);
expect(harness.ipcRenderer.invoke).toHaveBeenCalledWith(
'DB_UPSERT_APP_PLAYLIST',
malformedPlaylist
);
const markers = harness.getMarkers();
expect(
markers.map(
({ correlationState, invalidReason, phase, playlistId }) => ({
correlationState,
invalidReason,
phase,
playlistId,
})
)
).toEqual([
{
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
phase: 'start',
playlistId: null,
},
{
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
phase: 'success',
playlistId: null,
},
]);
expect(markers[0].ipcCallId).toBe(markers[1].ipcCallId);
expect(JSON.stringify(markers)).not.toContain(
'sentinel-malformed-playlist-id'
);
});
it('poisons correlation after the first marker delivery failure', async () => {
const payload = createRefreshPayload();
const result = createPlaylist(payload.playlistId);
await loadPerformancePreload();
harness.ipcRenderer.invoke
.mockResolvedValueOnce(result)
.mockResolvedValueOnce(result)
.mockResolvedValueOnce({ success: true });
harness.ipcRenderer.send.mockImplementationOnce((channel: string) => {
if (channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL) {
throw new Error('marker-send-failed');
}
});
await expect(harness.getApi().refreshPlaylist(payload)).resolves.toBe(
result
);
await expect(
harness
.getApi()
.dbGetAppPlaylist(payload.playlistId, payload.operationId)
).resolves.toBe(result);
await expect(
harness.getApi().dbUpsertAppPlaylist(result, payload.operationId)
).resolves.toEqual({ success: true });
expect(
harness.ipcRenderer.send.mock.calls.filter(
([channel]) => channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL
)
).toHaveLength(1);
expect(harness.getMarkers()).toHaveLength(1);
expect(harness.getMarkers()[0]).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
phase: 'start',
});
expect(
harness
.getMarkers()
.some(
({ correlationState }) =>
correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE
)
).toBe(false);
});
});
function markerPhases(
markers: PreloadPerformanceMarker[]
): [number, PreloadPerformanceMarker['phase']][] {
return markers.map(({ ipcCallId, phase }) => [ipcCallId, phase]);
}
@@ -0,0 +1,247 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type Settings,
Theme,
} from '@iptvnator/shared/interfaces';
import { redactSensitiveData } from '@iptvnator/shared/logging';
import { DEBUG_TRACE_EVENT_CHANNEL } from '../services/debug-trace';
import {
createPreloadPerformanceHarness,
PRELOAD_ENVIRONMENT,
type PreloadPerformanceHarness,
} from './main.preload.performance.test-helpers';
describe('main preload performance marker gates', () => {
let harness: PreloadPerformanceHarness;
beforeEach(() => {
harness = createPreloadPerformanceHarness();
});
afterEach(() => {
harness.cleanup();
jest.restoreAllMocks();
});
it.each([undefined, '', '0', 'false', 'no', 'off'])(
'emits no marker when the performance flag is %s',
async (flagValue) => {
await harness.load(
flagValue === undefined
? {}
: {
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: flagValue,
}
);
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(harness.getMarkers()).toEqual([]);
}
);
it.each(['1', ' TRUE ', 'yes', 'On'])(
'accepts the existing true flag value %s',
async (flagValue) => {
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: flagValue,
});
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(
harness.getMarkers().map(({ correlationState, phase }) => ({
correlationState,
phase,
}))
).toEqual([
{
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
phase: 'start',
},
{
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
phase: 'success',
},
]);
}
);
it('keeps trace-only calls on the existing debug channel', async () => {
await harness.load({
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
});
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(
harness
.getSentPayloads<
Record<string, unknown>
>(DEBUG_TRACE_EVENT_CHANNEL)
.map(({ method, phase }) => ({ method, phase }))
).toEqual([
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'start',
},
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'success',
},
]);
expect(harness.getMarkers()).toEqual([]);
});
it('keeps debug tracing unchanged when both gates are enabled', async () => {
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
});
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(
harness
.getSentPayloads<
Record<string, unknown>
>(DEBUG_TRACE_EVENT_CHANNEL)
.map(({ method, phase }) => ({ method, phase }))
).toEqual([
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'start',
},
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'success',
},
]);
expect(
harness.getSentPayloads(PRELOAD_PERFORMANCE_MARKER_CHANNEL).length
).toBe(2);
});
it('does not inspect or mark non-target calls in performance-only mode', async () => {
const result = { success: true };
const settings = Object.defineProperty({}, 'sentinel', {
enumerable: true,
get: () => {
throw new Error('non-target arguments must stay opaque');
},
}) as Partial<Settings>;
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
});
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
await expect(harness.getApi().updateSettings(settings)).resolves.toBe(
result
);
expect(harness.ipcRenderer.invoke).toHaveBeenCalledWith(
'SETTINGS_UPDATE',
settings
);
expect(harness.getMarkers()).toEqual([]);
});
it('preserves the full legacy trace payload for non-target success', async () => {
const settings = {
password: 'sentinel-argument-secret',
theme: Theme.DarkTheme,
} as Partial<Settings>;
const result = {
password: 'sentinel-result-secret',
success: true,
};
await harness.load({
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
});
jest.spyOn(globalThis.performance, 'now')
.mockReturnValueOnce(10)
.mockReturnValueOnce(12.34);
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
await expect(harness.getApi().updateSettings(settings)).resolves.toBe(
result
);
const traces = harness.getSentPayloads<Record<string, unknown>>(
DEBUG_TRACE_EVENT_CHANNEL
);
expect(traces).toEqual([
{
args: {
items: [
{
password: 'sentinel-argument-secret',
theme: Theme.DarkTheme,
},
],
length: 1,
type: 'array',
},
method: 'updateSettings',
phase: 'start',
},
{
durationMs: 2.3,
method: 'updateSettings',
phase: 'success',
result,
},
]);
const redacted = JSON.stringify(redactSensitiveData(traces));
expect(redacted).not.toContain('sentinel-argument-secret');
expect(redacted).not.toContain('sentinel-result-secret');
expect(harness.getMarkers()).toEqual([]);
});
it('preserves the full legacy trace payload for non-target errors', async () => {
const rejection = new Error('token=sentinel-error-secret');
await harness.load({
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
});
jest.spyOn(globalThis.performance, 'now')
.mockReturnValueOnce(20)
.mockReturnValueOnce(23.26);
harness.ipcRenderer.invoke.mockRejectedValueOnce(rejection);
await expect(
harness.getApi().updateSettings({ theme: Theme.LightTheme })
).rejects.toBe(rejection);
const traces = harness.getSentPayloads<Record<string, unknown>>(
DEBUG_TRACE_EVENT_CHANNEL
);
expect(traces).toEqual([
{
args: {
items: [{ theme: Theme.LightTheme }],
length: 1,
type: 'array',
},
method: 'updateSettings',
phase: 'start',
},
{
durationMs: 3.3,
error: {
message: rejection.message,
name: 'Error',
},
method: 'updateSettings',
phase: 'error',
},
]);
expect(JSON.stringify(redactSensitiveData(traces))).not.toContain(
'sentinel-error-secret'
);
expect(harness.getMarkers()).toEqual([]);
});
});
@@ -0,0 +1,79 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createPlaylist,
createPreloadPerformanceHarness,
createRefreshPayload,
PRELOAD_ENVIRONMENT,
type PreloadPerformanceHarness,
} from './main.preload.performance.test-helpers';
describe('main preload performance marker identities', () => {
let harness: PreloadPerformanceHarness;
beforeEach(async () => {
harness = createPreloadPerformanceHarness();
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
});
});
afterEach(() => {
harness.cleanup();
});
it('completes only for database calls carrying the refresh operation ID', async () => {
const payload = createRefreshPayload();
const playlist = createPlaylist(payload.playlistId);
harness.ipcRenderer.invoke.mockResolvedValue(playlist);
const api = harness.getApi();
await api.refreshPlaylist(payload);
await api.dbGetAppPlaylist(payload.playlistId);
await api.dbUpsertAppPlaylist(playlist);
await api.dbGetAppPlaylist(payload.playlistId, 'other-operation');
await api.dbUpsertAppPlaylist(playlist, 'other-operation');
expect(
harness
.getMarkers()
.slice(2)
.every(
({ correlationState }) =>
correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED
)
).toBe(true);
expect(
harness
.getMarkers()
.some(
({ correlationState }) =>
correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE
)
).toBe(false);
await api.dbGetAppPlaylist(payload.playlistId, payload.operationId);
await api.dbUpsertAppPlaylist(playlist, payload.operationId);
const markers = harness.getMarkers();
expect(markers.at(-1)).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: payload.operationId,
playlistId: payload.playlistId,
});
expect(harness.ipcRenderer.invoke.mock.calls).toEqual([
['PLAYLIST:REFRESH', payload],
['DB_GET_APP_PLAYLIST', payload.playlistId],
['DB_UPSERT_APP_PLAYLIST', playlist],
['DB_GET_APP_PLAYLIST', payload.playlistId],
['DB_UPSERT_APP_PLAYLIST', playlist],
['DB_GET_APP_PLAYLIST', payload.playlistId],
['DB_UPSERT_APP_PLAYLIST', playlist],
]);
});
});
@@ -0,0 +1,162 @@
import type {
ElectronBridgeApi,
Playlist,
PlaylistRefreshPayload,
PreloadPerformanceMarker,
} from '@iptvnator/shared/interfaces';
export const PRELOAD_ENVIRONMENT = {
PERF_CAPTURE: 'IPTVNATOR_PERF_CAPTURE',
TRACE_IPC: 'IPTVNATOR_TRACE_IPC',
TRACE_STARTUP: 'IPTVNATOR_TRACE_STARTUP',
} as const;
type PreloadEnvironmentName =
(typeof PRELOAD_ENVIRONMENT)[keyof typeof PRELOAD_ENVIRONMENT];
type PreloadEnvironment = Partial<Record<PreloadEnvironmentName, string>>;
export type ExposedElectronApi = ElectronBridgeApi &
Record<string, (...args: unknown[]) => unknown>;
export interface MockIpcRenderer {
invoke: jest.Mock;
off: jest.Mock;
on: jest.Mock;
send: jest.Mock;
}
export interface PreloadPerformanceHarness {
cleanup: () => void;
getApi: () => ExposedElectronApi;
getMarkers: () => PreloadPerformanceMarker[];
getSentPayloads: <T>(channel: string) => T[];
ipcRenderer: MockIpcRenderer;
load: (environment?: PreloadEnvironment) => Promise<void>;
}
const MARKER_KEYS = [
'correlationState',
'invalidReason',
'ipcCallId',
'method',
'operationId',
'phase',
'playlistId',
'sourceEpochMs',
] as const;
export function createPlaylist(
playlistId: string,
overrides: Partial<Playlist> = {}
): Playlist {
return {
_id: playlistId,
autoRefresh: false,
count: 1,
importDate: '2026-07-26T00:00:00.000Z',
lastUsage: '2026-07-26T00:00:00.000Z',
title: 'Performance fixture',
...overrides,
};
}
export function createRefreshPayload(
overrides: Partial<PlaylistRefreshPayload> = {}
): PlaylistRefreshPayload {
return {
operationId: 'operation-1',
playlistId: 'playlist-1',
title: 'Performance fixture',
...overrides,
};
}
export function expectFixedMarkers(markers: PreloadPerformanceMarker[]): void {
for (const [index, marker] of markers.entries()) {
expect(Object.keys(marker).sort()).toEqual([...MARKER_KEYS].sort());
expect(Number.isFinite(marker.sourceEpochMs)).toBe(true);
if (index > 0) {
expect(marker.sourceEpochMs).toBeGreaterThanOrEqual(
markers[index - 1].sourceEpochMs
);
}
}
}
export function createPreloadPerformanceHarness(): PreloadPerformanceHarness {
const originalEnvironment = new Map<
PreloadEnvironmentName,
string | undefined
>(
Object.values(PRELOAD_ENVIRONMENT).map((name) => [
name,
process.env[name],
])
);
let exposedApi: ExposedElectronApi | null = null;
const ipcRenderer: MockIpcRenderer = {
invoke: jest.fn(),
off: jest.fn(),
on: jest.fn(),
send: jest.fn(),
};
function getSentPayloads<T>(channel: string): T[] {
return ipcRenderer.send.mock.calls
.filter(([sentChannel]) => sentChannel === channel)
.map(([, payload]) => payload as T);
}
return {
cleanup: () => {
for (const [name, value] of originalEnvironment) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
jest.dontMock('electron');
},
getApi: () => {
if (!exposedApi) {
throw new Error('Expected preload API to be exposed');
}
return exposedApi;
},
getMarkers: () =>
getSentPayloads<PreloadPerformanceMarker>(
'IPTVNATOR_PERF_CAPTURE_MARKER'
),
getSentPayloads,
ipcRenderer,
load: async (environment = {}) => {
jest.resetModules();
for (const name of Object.values(PRELOAD_ENVIRONMENT)) {
delete process.env[name];
}
Object.assign(process.env, environment);
exposedApi = null;
ipcRenderer.invoke.mockReset().mockResolvedValue({ success: true });
ipcRenderer.off.mockReset();
ipcRenderer.on.mockReset();
ipcRenderer.send.mockReset();
jest.doMock('electron', () => ({
contextBridge: {
exposeInMainWorld: jest.fn(
(_name: string, api: ExposedElectronApi) => {
exposedApi = api;
}
),
},
ipcRenderer,
webUtils: {
getPathForFile: jest.fn(),
},
}));
await import('./main.preload');
},
};
}
@@ -11,6 +11,10 @@ import {
attachEmbeddedMpvFrameView,
detachEmbeddedMpvFrameView,
} from './embedded-mpv-frame-pump';
import {
createPreloadPerformanceCapture,
toPreloadPerformanceTargetMethod,
} from './preload-performance-capture';
import type {
EmbeddedMpvBounds,
EmbeddedMpvRecordingStartOptions,
@@ -48,6 +52,7 @@ import type {
} from '@iptvnator/shared/interfaces';
import {
DEBUG_TRACE_EVENT_CHANNEL,
isPerformanceCaptureEnabled,
isRendererApiTraceEnabled,
roundTraceDuration,
summarizeForTrace,
@@ -72,6 +77,11 @@ const dbSaveContentProgressListeners = new Set<
>();
const shouldTraceRendererApi = isRendererApiTraceEnabled();
const shouldCapturePerformance = isPerformanceCaptureEnabled();
const preloadPerformanceCapture = createPreloadPerformanceCapture(
shouldCapturePerformance,
(channel, marker) => ipcRenderer.send(channel, marker)
);
function emitRendererTrace(payload: {
method: string;
@@ -89,7 +99,7 @@ function emitRendererTrace(payload: {
}
function wrapElectronApi<T extends object>(api: T): T {
if (!shouldTraceRendererApi) {
if (!shouldTraceRendererApi && !shouldCapturePerformance) {
return api;
}
@@ -98,7 +108,9 @@ function wrapElectronApi<T extends object>(api: T): T {
if (
typeof value !== 'function' ||
name.startsWith('on') ||
name.startsWith('remove')
name.startsWith('remove') ||
(!shouldTraceRendererApi &&
!toPreloadPerformanceTargetMethod(name))
) {
return [name, value];
}
@@ -108,13 +120,20 @@ function wrapElectronApi<T extends object>(api: T): T {
return [
name,
(...args: unknown[]) => {
const startedAt =
globalThis.performance?.now?.() ?? Date.now();
emitRendererTrace({
args: summarizeForTrace(args),
method: name,
phase: 'start',
});
const startedAt = shouldTraceRendererApi
? (globalThis.performance?.now?.() ?? Date.now())
: 0;
if (shouldTraceRendererApi) {
emitRendererTrace({
args: summarizeForTrace(args),
method: name,
phase: 'start',
});
}
const performanceCall = preloadPerformanceCapture.start(
name,
args
);
try {
const result = original(...args);
@@ -126,54 +145,74 @@ function wrapElectronApi<T extends object>(api: T): T {
) {
return (result as Promise<unknown>)
.then((resolvedValue) => {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(
resolvedValue
),
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(
resolvedValue
),
});
}
preloadPerformanceCapture.success(
performanceCall,
resolvedValue
);
return resolvedValue;
})
.catch((error: unknown) => {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
}
preloadPerformanceCapture.error(
performanceCall
);
throw error;
});
}
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(result),
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(result),
});
}
preloadPerformanceCapture.success(
performanceCall,
result
);
return result;
} catch (error) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
}
preloadPerformanceCapture.error(performanceCall);
throw error;
}
},
@@ -530,8 +569,16 @@ const electronApi: ElectronBridgeApi = {
ipcRenderer.invoke('EPG_MAPPING_GET', { channelKey }),
getEpgMappingsBatch: (channelKeys: string[]) =>
ipcRenderer.invoke('EPG_MAPPING_GET_BATCH', { channelKeys }),
setEpgMapping: (channelKey: string, epgChannelId: string, playlistId?: string) =>
ipcRenderer.invoke('EPG_MAPPING_SET', { channelKey, epgChannelId, playlistId }),
setEpgMapping: (
channelKey: string,
epgChannelId: string,
playlistId?: string
) =>
ipcRenderer.invoke('EPG_MAPPING_SET', {
channelKey,
epgChannelId,
playlistId,
}),
deleteEpgMapping: (channelKey: string) =>
ipcRenderer.invoke('EPG_MAPPING_DELETE', { channelKey }),
searchEpgChannels: (searchTerm: string, limit?: number) =>
@@ -571,14 +618,14 @@ const electronApi: ElectronBridgeApi = {
ipcRenderer.invoke('DB_CREATE_PLAYLIST', playlist),
dbGetPlaylist: (playlistId: string) =>
ipcRenderer.invoke('DB_GET_PLAYLIST', playlistId),
dbUpsertAppPlaylist: (playlist: Playlist) =>
dbUpsertAppPlaylist: (playlist: Playlist, _operationId?: string) =>
ipcRenderer.invoke('DB_UPSERT_APP_PLAYLIST', playlist),
dbUpsertAppPlaylists: (playlists: Playlist[]) =>
ipcRenderer.invoke('DB_UPSERT_APP_PLAYLISTS', playlists),
dbGetAppPlaylists: () => ipcRenderer.invoke('DB_GET_APP_PLAYLISTS'),
dbGetAppPlaylistMetas: () =>
ipcRenderer.invoke('DB_GET_APP_PLAYLIST_METAS'),
dbGetAppPlaylist: (playlistId: string) =>
dbGetAppPlaylist: (playlistId: string, _operationId?: string) =>
ipcRenderer.invoke('DB_GET_APP_PLAYLIST', playlistId),
dbGetAppPlaylistFavoriteChannels: (playlistId: string) =>
ipcRenderer.invoke('DB_GET_APP_PLAYLIST_FAVORITE_CHANNELS', playlistId),
@@ -0,0 +1,50 @@
import {
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type PreloadPerformanceMarker,
} from '@iptvnator/shared/interfaces';
import { createPreloadPerformanceCapture } from './preload-performance-capture';
describe('preload performance capture timestamps', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('uses fractional monotonic time on top of the epoch origin', () => {
const markers: PreloadPerformanceMarker[] = [];
const monotonicNow = jest
.spyOn(globalThis.performance, 'now')
.mockReturnValueOnce(10.125)
.mockReturnValueOnce(10.375);
const capture = createPreloadPerformanceCapture(
true,
(channel, marker) => {
expect(channel).toBe(PRELOAD_PERFORMANCE_MARKER_CHANNEL);
markers.push(marker);
}
);
const call = capture.start(
PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
[
{
operationId: 'operation-1',
playlistId: 'playlist-1',
},
]
);
capture.success(call, {});
expect(monotonicNow).toHaveBeenCalledTimes(2);
expect(markers).toHaveLength(2);
expect(
markers[0].sourceEpochMs - globalThis.performance.timeOrigin
).toBeCloseTo(10.125, 3);
expect(
markers[1].sourceEpochMs - globalThis.performance.timeOrigin
).toBeCloseTo(10.375, 3);
expect(markers[1].sourceEpochMs).toBeGreaterThan(
markers[0].sourceEpochMs
);
});
});
@@ -0,0 +1,197 @@
import {
isPlaylistRefreshCancelledResult,
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type PreloadPerformanceMarker,
type PreloadPerformanceMethod,
type PreloadPerformancePhase,
} from '@iptvnator/shared/interfaces';
import {
advancePreloadPerformanceCorrelation,
createPreloadPerformanceCorrelationState,
normalizePreloadPerformanceIdentifier,
type PreloadPerformanceCorrelationState,
} from './preload-performance-correlation';
export interface PreloadPerformanceCallContext {
ipcCallId: number;
method: PreloadPerformanceMethod;
operationId: string | null;
playlistId: string | null;
}
export interface PreloadPerformanceCapture {
error: (call: PreloadPerformanceCallContext | null) => void;
start: (
methodName: string,
args: readonly unknown[]
) => PreloadPerformanceCallContext | null;
success: (
call: PreloadPerformanceCallContext | null,
result: unknown
) => void;
}
type MarkerSender = (
channel: typeof PRELOAD_PERFORMANCE_MARKER_CHANNEL,
marker: PreloadPerformanceMarker
) => void;
function readProperty(value: unknown, property: string): unknown {
if (
(typeof value !== 'object' || value === null) &&
typeof value !== 'function'
) {
return undefined;
}
try {
return Reflect.get(value, property);
} catch {
return undefined;
}
}
export function toPreloadPerformanceTargetMethod(
methodName: string
): PreloadPerformanceMethod | null {
switch (methodName) {
case PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST:
case PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST:
case PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST:
return methodName;
default:
return null;
}
}
function readHighResolutionEpochMs(): number {
const timeOrigin = globalThis.performance?.timeOrigin;
const monotonicNow = globalThis.performance?.now?.();
const highResolutionEpoch =
typeof timeOrigin === 'number' && typeof monotonicNow === 'number'
? timeOrigin + monotonicNow
: Number.NaN;
return Number.isFinite(highResolutionEpoch)
? highResolutionEpoch
: Date.now();
}
function extractIdentifiers(
method: PreloadPerformanceMethod,
args: readonly unknown[]
): Pick<PreloadPerformanceCallContext, 'operationId' | 'playlistId'> {
if (method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
const payload = args[0];
return {
operationId: normalizePreloadPerformanceIdentifier(
readProperty(payload, 'operationId')
),
playlistId: normalizePreloadPerformanceIdentifier(
readProperty(payload, 'playlistId')
),
};
}
if (method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST) {
return {
operationId: normalizePreloadPerformanceIdentifier(args[1]),
playlistId: normalizePreloadPerformanceIdentifier(args[0]),
};
}
return {
operationId: normalizePreloadPerformanceIdentifier(args[1]),
playlistId: normalizePreloadPerformanceIdentifier(
readProperty(args[0], '_id')
),
};
}
export function createPreloadPerformanceCapture(
enabled: boolean,
sendMarker: MarkerSender
): PreloadPerformanceCapture {
let correlationState: PreloadPerformanceCorrelationState =
createPreloadPerformanceCorrelationState();
let markerDeliveryFailed = false;
let lastSourceEpochMs = 0;
let nextIpcCallId = 1;
function emit(
call: PreloadPerformanceCallContext,
phase: PreloadPerformancePhase,
refreshCancelled = false
): void {
if (markerDeliveryFailed) {
return;
}
try {
const transition = advancePreloadPerformanceCorrelation(
correlationState,
{
...call,
phase,
refreshCancelled,
}
);
const sourceEpochMs = Math.max(
readHighResolutionEpochMs(),
lastSourceEpochMs
);
sendMarker(PRELOAD_PERFORMANCE_MARKER_CHANNEL, {
...transition.marker,
sourceEpochMs,
});
correlationState = transition.state;
lastSourceEpochMs = sourceEpochMs;
} catch {
markerDeliveryFailed = true;
// Performance instrumentation must never affect the bridge call.
}
}
return {
error: (call) => {
if (call) {
emit(call, 'error');
}
},
start: (methodName, args) => {
if (!enabled) {
return null;
}
const method = toPreloadPerformanceTargetMethod(methodName);
if (!method) {
return null;
}
const call: PreloadPerformanceCallContext = {
ipcCallId: nextIpcCallId,
method,
...extractIdentifiers(method, args),
};
nextIpcCallId += 1;
emit(call, 'start');
return call;
},
success: (call, result) => {
if (!call) {
return;
}
let refreshCancelled = false;
if (call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
try {
refreshCancelled = isPlaylistRefreshCancelledResult(result);
} catch {
refreshCancelled = false;
}
}
emit(call, 'success', refreshCancelled);
},
};
}
@@ -0,0 +1,246 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
PRELOAD_PERFORMANCE_SEQUENCE_STAGE,
closeInvalidPreloadPerformanceSequenceWhenSettled,
createPreloadPerformanceMarkerMetadata,
invalidatePreloadPerformanceSequence,
type PreloadPerformanceCall,
type PreloadPerformanceCorrelationEvent,
type PreloadPerformanceMarkerMetadata,
type PreloadPerformanceSequence,
} from './preload-performance-correlation.model';
function eventForRememberedCall(
event: PreloadPerformanceCorrelationEvent,
call: PreloadPerformanceCall
): PreloadPerformanceCorrelationEvent {
return {
...event,
method: call.method,
operationId: call.operationId,
playlistId: call.playlistId,
};
}
function invalidCompletion(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
call: PreloadPerformanceCall
): PreloadPerformanceMarkerMetadata {
const rememberedEvent = eventForRememberedCall(event, call);
const invalidSequence =
call.playlistId === null
? undefined
: invalidatePreloadPerformanceSequence(
calls,
sequences,
call.playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
calls.delete(event.ipcCallId);
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
call.playlistId
);
return createPreloadPerformanceMarkerMetadata(
rememberedEvent,
call.playlistId,
invalidSequence?.operationId ?? call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidSequence?.invalidReason ??
call.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
}
function completeCorrelatedCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
call: PreloadPerformanceCall & {
operationId: string;
playlistId: string;
}
): PreloadPerformanceMarkerMetadata {
const sequence = sequences.get(call.playlistId);
const expectedStage =
call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_STARTED
: call.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_STARTED
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_UPSERT_STARTED;
if (
!sequence ||
sequence.operationId !== call.operationId ||
sequence.stage !== expectedStage
) {
return invalidCompletion(calls, sequences, event, call);
}
if (event.phase === 'error') {
invalidatePreloadPerformanceSequence(
calls,
sequences,
call.playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR
);
calls.delete(event.ipcCallId);
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
call.playlistId
);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR
);
}
if (
call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST &&
event.refreshCancelled === true
) {
calls.delete(event.ipcCallId);
sequences.delete(call.playlistId);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
PRELOAD_PERFORMANCE_INVALID_REASON.REFRESH_CANCELLED
);
}
calls.delete(event.ipcCallId);
if (call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
sequences.set(call.playlistId, {
...sequence,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_SUCCEEDED,
});
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
if (call.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST) {
sequences.set(call.playlistId, {
...sequence,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_SUCCEEDED,
});
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
sequences.delete(call.playlistId);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
null
);
}
export function completePreloadPerformanceCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null
): PreloadPerformanceMarkerMetadata {
const call = calls.get(event.ipcCallId);
if (!call) {
const invalidSequence =
playlistId === null
? undefined
: invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
playlistId
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
invalidSequence?.operationId ?? operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidSequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
}
const identityMatches =
call.method === event.method &&
call.operationId === operationId &&
call.playlistId === playlistId;
if (!identityMatches) {
return invalidCompletion(calls, sequences, event, call);
}
const rememberedEvent = eventForRememberedCall(event, call);
if (
call.correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED
) {
calls.delete(event.ipcCallId);
return createPreloadPerformanceMarkerMetadata(
rememberedEvent,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
null
);
}
if (
call.correlationState === PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID
) {
calls.delete(event.ipcCallId);
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
call.playlistId
);
return createPreloadPerformanceMarkerMetadata(
rememberedEvent,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
call.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
}
return completeCorrelatedCall(
calls,
sequences,
rememberedEvent,
call as PreloadPerformanceCall & {
operationId: string;
playlistId: string;
}
);
}
@@ -0,0 +1,311 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createCorrelationHarness,
OPERATION_ONE,
OPERATION_TWO,
PLAYLIST_ONE,
PLAYLIST_TWO,
type CorrelationHarness,
} from './preload-performance-correlation.test-helpers';
function completeDatabaseSequence(
advance: CorrelationHarness['advance'],
firstCallId: number,
playlistId: string,
operationId: string
) {
return [
advance({
ipcCallId: firstCallId,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId,
phase: 'start',
playlistId,
}),
advance({
ipcCallId: firstCallId,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId,
phase: 'success',
playlistId,
}),
advance({
ipcCallId: firstCallId + 1,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId,
phase: 'start',
playlistId,
}),
advance({
ipcCallId: firstCallId + 1,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId,
phase: 'success',
playlistId,
}),
];
}
describe('preload performance marker correlation completions', () => {
let advance: CorrelationHarness['advance'];
beforeEach(() => {
({ advance } = createCorrelationHarness());
});
it.each(['success', 'error'] as const)(
'invalidates only the matching playlist after an unknown call ID %s',
(phase) => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 10,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_TWO,
});
const unknownCompletion = advance({
ipcCallId: 99,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase,
playlistId: PLAYLIST_ONE,
});
const matchingCompletion = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const samePlaylistPersistence = completeDatabaseSequence(
advance,
20,
PLAYLIST_ONE,
OPERATION_ONE
);
const unrelatedRefreshCompletion = advance({
ipcCallId: 10,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_TWO,
});
const unrelatedPersistence = completeDatabaseSequence(
advance,
30,
PLAYLIST_TWO,
OPERATION_TWO
);
expect(unknownCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
});
expect(matchingCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
expect(
[matchingCompletion, ...samePlaylistPersistence].some(
({ correlationState }) =>
correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE
)
).toBe(false);
expect(unrelatedRefreshCompletion.correlationState).toBe(
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
);
expect(unrelatedPersistence.at(-1)).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
operationId: OPERATION_TWO,
playlistId: PLAYLIST_TWO,
});
}
);
it('invalidates persistence after a duplicate completion without touching another playlist', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 10,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_TWO,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const duplicateCompletion = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const samePlaylistPersistence = completeDatabaseSequence(
advance,
20,
PLAYLIST_ONE,
OPERATION_ONE
);
advance({
ipcCallId: 10,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_TWO,
});
const unrelatedPersistence = completeDatabaseSequence(
advance,
30,
PLAYLIST_TWO,
OPERATION_TWO
);
expect(duplicateCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
});
expect(
samePlaylistPersistence.some(
({ correlationState }) =>
correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE
)
).toBe(false);
expect(unrelatedPersistence.at(-1)).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
operationId: OPERATION_TWO,
playlistId: PLAYLIST_TWO,
});
});
it('invalidates a correlated sequence when a target call errors', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const error = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'error',
playlistId: PLAYLIST_ONE,
});
const laterUpsert = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const restart = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(error).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR,
operationId: OPERATION_ONE,
});
expect(laterUpsert).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
operationId: OPERATION_ONE,
});
expect(restart.correlationState).toBe(
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
);
});
it('closes a cancelled refresh without awaiting database persistence', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const cancelled = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
refreshCancelled: true,
});
const laterGet = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(cancelled).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.REFRESH_CANCELLED,
operationId: OPERATION_ONE,
});
expect(laterGet).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
operationId: OPERATION_ONE,
});
});
});
@@ -0,0 +1,183 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createCorrelationHarness,
OPERATION_ONE,
OPERATION_TWO,
PLAYLIST_ONE,
PLAYLIST_TWO,
} from './preload-performance-correlation.test-helpers';
describe('preload performance correlation identities', () => {
it('advances only database calls tagged with the refresh operation', () => {
const { advance } = createCorrelationHarness();
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const unrelated = [
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
}),
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
}),
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
}),
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_ONE,
}),
];
expect(
unrelated.every(
({ correlationState }) =>
correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED
)
).toBe(true);
for (const event of [
{
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'start',
},
{
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'success',
},
{
ipcCallId: 5,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
phase: 'start',
},
] as const) {
advance({
...event,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
});
}
const complete = advance({
ipcCallId: 5,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(complete).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
});
});
it.each([
{
label: 'operation',
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_TWO,
playlistId: PLAYLIST_ONE,
},
{
label: 'playlist',
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_TWO,
},
{
label: 'method',
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
},
])(
'fails closed when the completion changes the started $label',
({ method, operationId, playlistId }) => {
const { advance } = createCorrelationHarness();
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const completion = advance({
ipcCallId: 2,
method,
operationId,
phase: 'success',
playlistId,
});
const laterUpsert = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(completion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
});
expect(laterUpsert).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
});
}
);
});
@@ -0,0 +1,173 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
type PreloadPerformanceCorrelationStateName,
type PreloadPerformanceInvalidReason,
type PreloadPerformanceMarker,
type PreloadPerformanceMethod,
type PreloadPerformancePhase,
} from '@iptvnator/shared/interfaces';
const MAX_IDENTIFIER_LENGTH = 128;
const SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
export const PRELOAD_PERFORMANCE_SEQUENCE_STAGE = {
DB_GET_STARTED: 'db-get-started',
DB_GET_SUCCEEDED: 'db-get-succeeded',
DB_UPSERT_STARTED: 'db-upsert-started',
INVALID: 'invalid',
REFRESH_STARTED: 'refresh-started',
REFRESH_SUCCEEDED: 'refresh-succeeded',
} as const;
export type PreloadPerformanceSequenceStage =
(typeof PRELOAD_PERFORMANCE_SEQUENCE_STAGE)[keyof typeof PRELOAD_PERFORMANCE_SEQUENCE_STAGE];
export interface PreloadPerformanceSequence {
invalidReason: PreloadPerformanceInvalidReason | null;
operationId: string;
stage: PreloadPerformanceSequenceStage;
}
export interface PreloadPerformanceCall {
correlationState: PreloadPerformanceCorrelationStateName;
invalidReason: PreloadPerformanceInvalidReason | null;
method: PreloadPerformanceMethod;
operationId: string | null;
playlistId: string | null;
}
export interface PreloadPerformanceCorrelationEvent {
ipcCallId: number;
method: PreloadPerformanceMethod;
operationId: unknown;
phase: PreloadPerformancePhase;
playlistId: unknown;
refreshCancelled?: boolean;
}
export interface PreloadPerformanceCorrelationState {
readonly calls: ReadonlyMap<number, PreloadPerformanceCall>;
readonly sequences: ReadonlyMap<string, PreloadPerformanceSequence>;
}
export interface PreloadPerformanceCorrelationResult {
marker: PreloadPerformanceMarkerMetadata;
state: PreloadPerformanceCorrelationState;
}
export type PreloadPerformanceMarkerMetadata = Omit<
PreloadPerformanceMarker,
'sourceEpochMs'
>;
export function normalizePreloadPerformanceIdentifier(
value: unknown
): string | null {
if (
typeof value !== 'string' ||
value.length === 0 ||
value.length > MAX_IDENTIFIER_LENGTH ||
value !== value.trim() ||
!SAFE_IDENTIFIER_PATTERN.test(value)
) {
return null;
}
return value;
}
export function createPreloadPerformanceMarkerMetadata(
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null,
correlationState: PreloadPerformanceCorrelationStateName,
invalidReason: PreloadPerformanceInvalidReason | null
): PreloadPerformanceMarkerMetadata {
return {
correlationState,
invalidReason,
ipcCallId: event.ipcCallId,
method: event.method,
operationId,
phase: event.phase,
playlistId,
};
}
export function createInvalidPreloadPerformanceCall(
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null,
invalidReason: PreloadPerformanceInvalidReason
): PreloadPerformanceCall {
return {
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason,
method: event.method,
operationId,
playlistId,
};
}
export function invalidatePreloadPerformanceSequence(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
playlistId: string,
reason: PreloadPerformanceInvalidReason
): PreloadPerformanceSequence | undefined {
const sequence = sequences.get(playlistId);
if (!sequence) {
return undefined;
}
const invalidReason =
sequence.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
? (sequence.invalidReason ?? reason)
: reason;
const invalidSequence: PreloadPerformanceSequence = {
...sequence,
invalidReason,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID,
};
sequences.set(playlistId, invalidSequence);
for (const [callId, call] of calls) {
if (
call.playlistId === playlistId &&
call.correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
) {
calls.set(callId, {
...call,
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason,
});
}
}
return invalidSequence;
}
export function closeInvalidPreloadPerformanceSequenceWhenSettled(
calls: ReadonlyMap<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
playlistId: string | null
): void {
if (
playlistId === null ||
sequences.get(playlistId)?.stage !==
PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
) {
return;
}
const hasInvalidCall = Array.from(calls.values()).some(
(call) =>
call.playlistId === playlistId &&
call.correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID
);
if (!hasInvalidCall) {
sequences.delete(playlistId);
}
}
@@ -0,0 +1,338 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createCorrelationHarness,
OPERATION_ONE,
OPERATION_TWO,
PLAYLIST_ONE,
PLAYLIST_TWO,
type CorrelationEvent,
type CorrelationHarness,
} from './preload-performance-correlation.test-helpers';
describe('preload performance marker correlation sequences', () => {
let advance: CorrelationHarness['advance'];
let reset: CorrelationHarness['reset'];
beforeEach(() => {
({ advance, reset } = createCorrelationHarness());
});
it('correlates the successful refresh persistence sequence and propagates its operation ID', () => {
const refreshStart = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const refreshSuccess = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const getStart = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const getSuccess = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const upsertStart = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const upsertSuccess = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(refreshStart).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
});
expect(refreshSuccess).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
operationId: OPERATION_ONE,
});
expect(getStart.operationId).toBe(OPERATION_ONE);
expect(getSuccess.operationId).toBe(OPERATION_ONE);
expect(upsertStart.operationId).toBe(OPERATION_ONE);
expect(upsertSuccess).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
invalidReason: null,
operationId: OPERATION_ONE,
});
});
it('fails closed for wrong-order and duplicate targeted database calls', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const wrongOrder = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(wrongOrder).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
operationId: OPERATION_ONE,
});
reset();
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const duplicate = advance({
ipcCallId: 5,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const originalCompletion = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(duplicate.invalidReason).toBe(
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
expect(originalCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
});
it('invalidates concurrent refreshes for the same playlist', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const concurrentStart = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const firstCompletion = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const concurrentCompletion = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const restart = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: 'operation-3',
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(concurrentStart).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH,
operationId: OPERATION_TWO,
});
expect(concurrentCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH,
operationId: OPERATION_TWO,
});
expect(restart.correlationState).toBe(
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
);
expect(firstCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH,
});
});
it('keeps different playlists independent when one sequence invalidates', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_TWO,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_TWO,
});
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const playlistTwoGet = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_TWO,
});
expect(playlistTwoGet).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
operationId: OPERATION_TWO,
playlistId: PLAYLIST_TWO,
});
});
it('allows a new refresh after the prior sequence completes', () => {
const events: CorrelationEvent[] = [
{
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
},
];
events.forEach((event) => advance(event));
const restart = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(restart).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
operationId: OPERATION_TWO,
});
});
});
@@ -0,0 +1,235 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
PRELOAD_PERFORMANCE_SEQUENCE_STAGE,
createInvalidPreloadPerformanceCall,
createPreloadPerformanceMarkerMetadata,
invalidatePreloadPerformanceSequence,
type PreloadPerformanceCall,
type PreloadPerformanceCorrelationEvent,
type PreloadPerformanceMarkerMetadata,
type PreloadPerformanceSequence,
} from './preload-performance-correlation.model';
function rememberUncorrelatedCall(
calls: Map<number, PreloadPerformanceCall>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string,
operationId: string | null
): PreloadPerformanceMarkerMetadata {
calls.set(event.ipcCallId, {
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
method: event.method,
operationId,
playlistId,
});
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
null
);
}
function startRefresh(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string,
operationId: string
): PreloadPerformanceMarkerMetadata {
const existing = sequences.get(playlistId);
if (existing) {
const reason =
existing.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
? (existing.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER)
: PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH;
const invalidSequence = invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
reason
);
const invalidReason = invalidSequence?.invalidReason ?? reason;
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
operationId,
invalidReason
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason
);
}
sequences.set(playlistId, {
invalidReason: null,
operationId,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_STARTED,
});
calls.set(event.ipcCallId, {
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
method: event.method,
operationId,
playlistId,
});
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
function startDatabaseCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string,
operationId: string | null
): PreloadPerformanceMarkerMetadata {
if (operationId === null) {
return rememberUncorrelatedCall(calls, event, playlistId, operationId);
}
const sequence = sequences.get(playlistId);
if (!sequence || sequence.operationId !== operationId) {
return rememberUncorrelatedCall(calls, event, playlistId, operationId);
}
if (sequence.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID) {
const invalidReason =
sequence.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER;
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
operationId,
invalidReason
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason
);
}
const isGet =
event.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST;
const expectedStage = isGet
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_SUCCEEDED
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_SUCCEEDED;
const startedStage = isGet
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_STARTED
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_UPSERT_STARTED;
if (sequence.stage !== expectedStage) {
const invalidSequence = invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
const invalidReason =
invalidSequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER;
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
operationId,
invalidReason
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason
);
}
sequences.set(playlistId, {
...sequence,
stage: startedStage,
});
calls.set(event.ipcCallId, {
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
method: event.method,
operationId,
playlistId,
});
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
export function startPreloadPerformanceCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null
): PreloadPerformanceMarkerMetadata {
if (
playlistId === null ||
(event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST &&
operationId === null)
) {
if (playlistId !== null) {
invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER
);
}
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
null,
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
null,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER
);
}
return event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
? startRefresh(calls, sequences, event, playlistId, operationId)
: startDatabaseCall(calls, sequences, event, playlistId, operationId);
}
@@ -0,0 +1,29 @@
import {
advancePreloadPerformanceCorrelation,
createPreloadPerformanceCorrelationState,
type PreloadPerformanceCorrelationEvent,
} from './preload-performance-correlation';
export const PLAYLIST_ONE = 'playlist-1';
export const PLAYLIST_TWO = 'playlist-2';
export const OPERATION_ONE = 'operation-1';
export const OPERATION_TWO = 'operation-2';
export type CorrelationEvent = PreloadPerformanceCorrelationEvent;
export function createCorrelationHarness() {
let state = createPreloadPerformanceCorrelationState();
return {
advance: (event: PreloadPerformanceCorrelationEvent) => {
const result = advancePreloadPerformanceCorrelation(state, event);
state = result.state;
return result.marker;
},
reset: () => {
state = createPreloadPerformanceCorrelationState();
},
};
}
export type CorrelationHarness = ReturnType<typeof createCorrelationHarness>;
@@ -0,0 +1,58 @@
import { completePreloadPerformanceCall } from './preload-performance-correlation.complete';
import {
normalizePreloadPerformanceIdentifier,
type PreloadPerformanceCorrelationEvent,
type PreloadPerformanceCorrelationResult,
type PreloadPerformanceCorrelationState,
} from './preload-performance-correlation.model';
import { startPreloadPerformanceCall } from './preload-performance-correlation.start';
export type {
PreloadPerformanceCorrelationEvent,
PreloadPerformanceCorrelationResult,
PreloadPerformanceCorrelationState,
} from './preload-performance-correlation.model';
export { normalizePreloadPerformanceIdentifier } from './preload-performance-correlation.model';
export function createPreloadPerformanceCorrelationState(): PreloadPerformanceCorrelationState {
return {
calls: new Map(),
sequences: new Map(),
};
}
export function advancePreloadPerformanceCorrelation(
state: PreloadPerformanceCorrelationState,
event: PreloadPerformanceCorrelationEvent
): PreloadPerformanceCorrelationResult {
const calls = new Map(state.calls);
const sequences = new Map(state.sequences);
const playlistId = normalizePreloadPerformanceIdentifier(event.playlistId);
const operationId = normalizePreloadPerformanceIdentifier(
event.operationId
);
const marker =
event.phase === 'start'
? startPreloadPerformanceCall(
calls,
sequences,
event,
playlistId,
operationId
)
: completePreloadPerformanceCall(
calls,
sequences,
event,
playlistId,
operationId
);
return {
marker,
state: {
calls,
sequences,
},
};
}
@@ -0,0 +1,117 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createCorrelationHarness,
OPERATION_ONE,
OPERATION_TWO,
PLAYLIST_ONE,
type CorrelationHarness,
} from './preload-performance-correlation.test-helpers';
describe('preload performance marker correlation validation', () => {
let advance: CorrelationHarness['advance'];
beforeEach(() => {
({ advance } = createCorrelationHarness());
});
it('uses null identifiers and a fixed reason for malformed inputs', () => {
const malformedPlaylist = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: 'https://secret.example/private.m3u',
});
const malformedOperation = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: { secret: OPERATION_TWO },
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const malformedDbId = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: undefined,
});
const malformedPlaylistCompletion = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: 'https://secret.example/private.m3u',
});
expect(malformedPlaylist).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: null,
});
expect(malformedOperation).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: PLAYLIST_ONE,
});
expect(malformedDbId).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: null,
});
expect(malformedPlaylistCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: null,
});
});
it('keeps targeted database calls outside a sequence visible and uncorrelated', () => {
const getStart = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const getSuccess = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(getStart).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
operationId: null,
});
expect(getSuccess).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
operationId: null,
});
});
});
@@ -52,6 +52,10 @@ export function isRendererApiTraceEnabled(): boolean {
return isStartupTraceEnabled() || readFlag('IPTVNATOR_TRACE_IPC');
}
export function isPerformanceCaptureEnabled(): boolean {
return readFlag('IPTVNATOR_PERF_CAPTURE');
}
export function isDbTraceEnabled(): boolean {
return isStartupTraceEnabled() || readFlag('IPTVNATOR_TRACE_DB');
}
@@ -90,8 +90,11 @@ import {
} from '../database/operations/xtream.operations';
import {
armWorkerPerformanceCapture,
finishWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
registerDatabaseWorkerPerformanceCapture,
releaseDatabaseWorkerPerformanceCapture,
startWorkerPerformanceCapture,
type WorkerPerformanceCapture,
} from './worker-performance-capture';
const loggerLabel = '[DB Worker]';
@@ -104,6 +107,11 @@ type ActiveOperationState = {
cancelled: boolean;
};
type PreRegisteredActiveOperation = {
operationId: string;
state: ActiveOperationState;
};
type OperationController = {
control: {
checkpoint: () => Promise<void>;
@@ -129,6 +137,7 @@ type OperationController = {
};
const activeOperations = new Map<string, ActiveOperationState>();
const activePerformanceCaptures = new Set<WorkerPerformanceCapture>();
if (!parentPort) {
throw new Error('Database worker must be started with a parent port');
@@ -178,17 +187,22 @@ async function pauseBetweenBatches(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, batchDelayMs));
}
function createOperationController(config: {
operationId?: string;
operation: string;
playlistId?: string;
requestId: string;
cancellable?: boolean;
}): OperationController {
function createOperationController(
config: {
operationId?: string;
operation: string;
playlistId?: string;
requestId: string;
cancellable?: boolean;
},
preRegisteredState?: ActiveOperationState
): OperationController {
const { operationId, operation, playlistId, requestId } = config;
const cancellable = config.cancellable ?? true;
const activeState =
operationId && cancellable ? { cancelled: false } : null;
operationId && cancellable
? (preRegisteredState ?? { cancelled: false })
: null;
if (operationId && activeState) {
activeOperations.set(operationId, activeState);
@@ -255,7 +269,10 @@ function createOperationController(config: {
});
},
cleanup: () => {
if (operationId) {
if (
operationId &&
activeOperations.get(operationId) === activeState
) {
activeOperations.delete(operationId);
}
},
@@ -264,9 +281,10 @@ function createOperationController(config: {
async function executeTrackedOperation<TResult>(
config: Parameters<typeof createOperationController>[0],
handler: (controller: OperationController) => Promise<TResult>
handler: (controller: OperationController) => Promise<TResult>,
preRegisteredState?: ActiveOperationState
): Promise<TResult> {
const controller = createOperationController(config);
const controller = createOperationController(config, preRegisteredState);
try {
return await handler(controller);
@@ -282,7 +300,53 @@ async function executeTrackedOperation<TResult>(
}
}
async function executeRequest(message: DbWorkerRequestMessage) {
function preRegisterCancellableOperation(
message: DbWorkerRequestMessage
): PreRegisteredActiveOperation | null {
switch (message.operation) {
case 'DB_SAVE_CONTENT':
case 'DB_DELETE_PLAYLIST':
case 'DB_DELETE_XTREAM_CONTENT':
case 'DB_RESTORE_XTREAM_USER_DATA':
break;
default:
return null;
}
if (
typeof message.payload !== 'object' ||
message.payload === null ||
Array.isArray(message.payload)
) {
return null;
}
const operationId = (message.payload as Record<string, unknown>)[
'operationId'
];
if (typeof operationId !== 'string' || operationId.length === 0) {
return null;
}
const state: ActiveOperationState = { cancelled: false };
activeOperations.set(operationId, state);
return { operationId, state };
}
function releasePreRegisteredOperation(
operation: PreRegisteredActiveOperation | null
): void {
if (
operation &&
activeOperations.get(operation.operationId) === operation.state
) {
activeOperations.delete(operation.operationId);
}
}
async function executeRequest(
message: DbWorkerRequestMessage,
preRegisteredState?: ActiveOperationState
) {
const db = await getWorkerDatabase();
switch (message.operation) {
@@ -413,7 +477,8 @@ async function executeRequest(message: DbWorkerRequestMessage) {
});
return result;
}
},
preRegisteredState
);
}
@@ -586,7 +651,8 @@ async function executeRequest(message: DbWorkerRequestMessage) {
});
return result;
}
},
preRegisteredState
);
}
@@ -695,7 +761,8 @@ async function executeRequest(message: DbWorkerRequestMessage) {
});
return result;
}
},
preRegisteredState
);
}
@@ -739,7 +806,8 @@ async function executeRequest(message: DbWorkerRequestMessage) {
});
return result;
}
},
preRegisteredState
);
}
@@ -929,33 +997,49 @@ parentPort.on('message', async (message: DbWorkerIncomingMessage) => {
return;
}
const performanceCapture = startWorkerPerformanceCapture();
await armWorkerPerformanceCapture(performanceCapture);
try {
const result = await executeRequest(message);
const performance =
await finishWorkerPerformanceCapture(performanceCapture);
const preRegisteredOperation = preRegisterCancellableOperation(message);
let performanceCapture: WorkerPerformanceCapture | null = null;
const execution = await (async () => {
try {
performanceCapture = startWorkerPerformanceCapture();
registerDatabaseWorkerPerformanceCapture(
activePerformanceCaptures,
performanceCapture
);
await armWorkerPerformanceCapture(performanceCapture);
return await executeWithWorkerPerformanceCapture(
performanceCapture,
() => executeRequest(message, preRegisteredOperation?.state)
);
} finally {
releaseDatabaseWorkerPerformanceCapture(
activePerformanceCaptures,
performanceCapture
);
releasePreRegisteredOperation(preRegisteredOperation);
}
})();
if (execution.success) {
postMessage({
type: 'response',
requestId: message.requestId,
success: true,
result,
performance,
result: execution.result,
performance: execution.performance,
});
} catch (error) {
} else {
console.error(
loggerLabel,
`Error handling ${message.operation}:`,
error
execution.error
);
const performance =
await finishWorkerPerformanceCapture(performanceCapture);
postMessage({
type: 'response',
requestId: message.requestId,
success: false,
error: serializeError(error),
performance,
error: serializeError(execution.error),
performance: execution.performance,
});
}
});
@@ -24,7 +24,7 @@ import { requestWithValidatedRedirects } from '../util/validated-axios';
import { PLAYLIST_FETCH_TIMEOUT_MS } from '../events/playlist-source';
import {
armWorkerPerformanceCapture,
finishWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
startWorkerPerformanceCapture,
} from './worker-performance-capture';
@@ -86,6 +86,15 @@ function checkpoint(payload: PlaylistRefreshPayload): void {
}
}
function releaseActiveRefresh(
operationId: string,
activeRefresh: ActiveRefreshState
): void {
if (activeRefreshes.get(operationId) === activeRefresh) {
activeRefreshes.delete(operationId);
}
}
async function fetchPlaylistFromUrl(
payload: PlaylistRefreshPayload,
controller: AbortController
@@ -159,23 +168,32 @@ async function fetchPlaylistFromFile(
}
async function executeRefresh(
payload: PlaylistRefreshPayload
payload: PlaylistRefreshPayload,
activeRefresh: ActiveRefreshState
): Promise<Playlist> {
const controller = new AbortController();
activeRefreshes.set(payload.operationId, {
cancelled: false,
controller,
});
try {
const playlist = payload.url
? await fetchPlaylistFromUrl(payload, controller)
? await fetchPlaylistFromUrl(payload, activeRefresh.controller)
: await fetchPlaylistFromFile(payload);
checkpoint(payload);
return playlist;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
emitEvent(payload, {
status: 'cancelled',
phase: 'parsing',
});
} else {
emitEvent(payload, {
status: 'error',
phase: payload.url ? 'fetching' : 'reading-file',
error: error instanceof Error ? error.message : String(error),
});
}
throw error;
} finally {
activeRefreshes.delete(payload.operationId);
releaseActiveRefresh(payload.operationId, activeRefresh);
}
}
@@ -191,39 +209,39 @@ parentPort.on(
return;
}
const performanceCapture = startWorkerPerformanceCapture();
await armWorkerPerformanceCapture(performanceCapture);
const payload = message.payload;
const activeRefresh: ActiveRefreshState = {
cancelled: false,
controller: new AbortController(),
};
activeRefreshes.set(payload.operationId, activeRefresh);
try {
const result = await executeRefresh(message.payload);
const performance =
await finishWorkerPerformanceCapture(performanceCapture);
postMessage({
type: 'response',
success: true,
result,
performance,
});
} catch (error) {
const payload = message.payload;
if (error instanceof Error && error.name === 'AbortError') {
emitEvent(payload, { status: 'cancelled', phase: 'parsing' });
const performanceCapture = startWorkerPerformanceCapture();
await armWorkerPerformanceCapture(performanceCapture);
const execution = await executeWithWorkerPerformanceCapture(
performanceCapture,
() => executeRefresh(payload, activeRefresh)
);
if (execution.success) {
postMessage({
type: 'response',
success: true,
result: execution.result,
performance: execution.performance,
});
} else {
emitEvent(payload, {
status: 'error',
phase: payload.url ? 'fetching' : 'reading-file',
error:
error instanceof Error ? error.message : String(error),
const error = execution.error;
postMessage({
type: 'response',
success: false,
error: serializeError(error),
performance: execution.performance,
});
}
const performance =
await finishWorkerPerformanceCapture(performanceCapture);
postMessage({
type: 'response',
success: false,
error: serializeError(error),
performance,
});
} finally {
releaseActiveRefresh(payload.operationId, activeRefresh);
}
}
);
@@ -0,0 +1,313 @@
import type {
DbWorkerIncomingMessage,
DbWorkerMessage,
} from './database-worker.types';
import type {
PlaylistRefreshWorkerIncomingMessage,
PlaylistRefreshWorkerMessage,
} from './playlist-refresh.worker.types';
type WorkerMessageHandler<TMessage> = (
message: TMessage
) => Promise<void> | void;
interface ArmGate {
readonly promise: Promise<void>;
readonly release: () => void;
}
function createArmGate(): ArmGate {
let release = (): void => undefined;
const promise = new Promise<void>((resolvePromise) => {
release = resolvePromise;
});
return { promise, release };
}
function createParentPortHarness<TIncoming, TOutgoing>() {
let messageHandler: WorkerMessageHandler<TIncoming> | null = null;
const postMessage = jest.fn<void, [TOutgoing]>();
const parentPort = {
on: jest.fn(
(event: string, handler: WorkerMessageHandler<TIncoming>): void => {
if (event === 'message') {
messageHandler = handler;
}
}
),
postMessage,
};
return {
getMessageHandler: (): WorkerMessageHandler<TIncoming> => {
if (!messageHandler) {
throw new Error('Worker message handler was not registered');
}
return messageHandler;
},
parentPort,
postMessage,
};
}
function mockPerformanceCapture(
armGate: ArmGate,
failureGates?: {
readonly observed: ArmGate;
readonly profilingFinished: ArmGate;
}
): void {
jest.doMock('./worker-performance-capture', () => ({
armWorkerPerformanceCapture: jest.fn(() => armGate.promise),
executeWithWorkerPerformanceCapture: jest.fn(
async (_capture: unknown, execute: () => Promise<unknown>) => {
try {
return {
error: null,
performance: undefined,
result: await execute(),
success: true,
};
} catch (error) {
failureGates?.observed.release();
await failureGates?.profilingFinished.promise;
return {
error,
performance: undefined,
result: undefined,
success: false,
};
}
}
),
registerDatabaseWorkerPerformanceCapture: jest.fn(),
releaseDatabaseWorkerPerformanceCapture: jest.fn(),
startWorkerPerformanceCapture: jest.fn(() => ({})),
}));
}
describe('worker cancellation while performance capture arms', () => {
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it('cancels playlist work before file access when cancel arrives during arming', async () => {
const armGate = createArmGate();
const profilingFinished = createArmGate();
const failureObserved = createArmGate();
const port = createParentPortHarness<
PlaylistRefreshWorkerIncomingMessage,
PlaylistRefreshWorkerMessage<unknown>
>();
const readFile = jest.fn().mockResolvedValue('#EXTM3U');
mockPerformanceCapture(armGate, {
observed: failureObserved,
profilingFinished,
});
jest.doMock('worker_threads', () => ({
parentPort: port.parentPort,
}));
jest.doMock('node:fs/promises', () => ({ readFile }));
jest.doMock('iptv-playlist-parser', () => ({
parse: jest.fn(() => ({ items: [] })),
}));
jest.doMock('@iptvnator/shared/m3u-utils', () => ({
createPlaylistObject: jest.fn(() => ({ id: 'playlist-1' })),
getFilenameFromUrl: jest.fn(() => 'playlist.m3u'),
}));
await import('./playlist-refresh.worker');
port.postMessage.mockClear();
const handleMessage = port.getMessageHandler();
const requestPromise = handleMessage({
type: 'request',
payload: {
filePath: '/fixtures/playlist.m3u',
operationId: 'refresh-1',
playlistId: 'playlist-1',
title: 'Fixture',
},
});
await handleMessage({
type: 'cancel',
operationId: 'refresh-1',
});
armGate.release();
await failureObserved.promise;
const cancelledBeforeProfilingFinished =
port.postMessage.mock.calls.some(
([message]) =>
message.type === 'event' &&
message.event.status === 'cancelled'
);
const responseBeforeProfilingFinished =
port.postMessage.mock.calls.some(
([message]) => message.type === 'response'
);
profilingFinished.release();
await requestPromise;
expect(readFile).not.toHaveBeenCalled();
expect(cancelledBeforeProfilingFinished).toBe(true);
expect(responseBeforeProfilingFinished).toBe(false);
expect(port.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
operationId: 'refresh-1',
status: 'cancelled',
}),
type: 'event',
})
);
expect(port.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({ name: 'AbortError' }),
success: false,
type: 'response',
})
);
});
it('cancels cancellable database work when cancel arrives during arming', async () => {
const armGate = createArmGate();
const port = createParentPortHarness<
DbWorkerIncomingMessage,
DbWorkerMessage
>();
const saveContent = jest.fn(
async (
_db: unknown,
_playlistId: string,
_streams: unknown[],
_type: string,
control: { checkpoint: () => Promise<void> }
) => {
await control.checkpoint();
return { count: 0 };
}
);
mockPerformanceCapture(armGate);
jest.doMock('worker_threads', () => ({
parentPort: port.parentPort,
}));
jest.doMock('./database.worker-connection', () => ({
closeWorkerDatabase: jest.fn(),
getWorkerDatabase: jest.fn().mockResolvedValue({}),
}));
jest.doMock('../database/operations/content.operations', () => ({
saveContent,
}));
jest.spyOn(console, 'error').mockImplementation(() => undefined);
await import('./database.worker');
port.postMessage.mockClear();
const handleMessage = port.getMessageHandler();
const requestPromise = handleMessage({
type: 'request',
operation: 'DB_SAVE_CONTENT',
payload: {
operationId: 'database-operation-1',
playlistId: 'playlist-1',
streams: [],
type: 'movie',
},
requestId: 'request-1',
});
await handleMessage({
type: 'cancel',
operationId: 'database-operation-1',
});
armGate.release();
await requestPromise;
expect(saveContent).toHaveBeenCalledTimes(1);
expect(port.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
operationId: 'database-operation-1',
status: 'cancelled',
}),
requestId: 'request-1',
type: 'event',
})
);
expect(port.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
error: expect.objectContaining({ name: 'AbortError' }),
requestId: 'request-1',
success: false,
type: 'response',
})
);
});
it('does not make an explicitly non-cancellable database operation cancellable during arming', async () => {
const armGate = createArmGate();
const port = createParentPortHarness<
DbWorkerIncomingMessage,
DbWorkerMessage
>();
const deleteAllPlaylists = jest.fn(
async (
_db: unknown,
control: { checkpoint: () => Promise<void> }
) => {
await control.checkpoint();
return { success: true };
}
);
mockPerformanceCapture(armGate);
jest.doMock('worker_threads', () => ({
parentPort: port.parentPort,
}));
jest.doMock('./database.worker-connection', () => ({
closeWorkerDatabase: jest.fn(),
getWorkerDatabase: jest.fn().mockResolvedValue({}),
}));
jest.doMock('../database/operations/playlist.operations', () => ({
deleteAllPlaylists,
}));
jest.spyOn(console, 'error').mockImplementation(() => undefined);
await import('./database.worker');
port.postMessage.mockClear();
const handleMessage = port.getMessageHandler();
const requestPromise = handleMessage({
type: 'request',
operation: 'DB_DELETE_ALL_PLAYLISTS',
payload: {
operationId: 'database-operation-2',
},
requestId: 'request-2',
});
await handleMessage({
type: 'cancel',
operationId: 'database-operation-2',
});
armGate.release();
await requestPromise;
expect(deleteAllPlaylists).toHaveBeenCalledTimes(1);
expect(port.postMessage).toHaveBeenCalledWith(
expect.objectContaining({
requestId: 'request-2',
result: { success: true },
success: true,
type: 'response',
})
);
expect(port.postMessage).not.toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({ status: 'cancelled' }),
requestId: 'request-2',
type: 'event',
})
);
});
});
@@ -0,0 +1,95 @@
import {
WORKER_PERFORMANCE_INVALID_REASON,
armWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
registerDatabaseWorkerPerformanceCapture,
releaseDatabaseWorkerPerformanceCapture,
startWorkerPerformanceCapture,
type WorkerPerformanceCapture,
} from './worker-performance-capture';
import { createFakeRuntime } from './worker-performance-capture.test-harness';
const PROFILING_ENV = 'IPTVNATOR_PERF_WORKER_PROFILING';
describe('worker performance capture concurrency and real timers', () => {
const originalProfilingValue = process.env[PROFILING_ENV];
afterEach(() => {
if (originalProfilingValue === undefined) {
delete process.env[PROFILING_ENV];
} else {
process.env[PROFILING_ENV] = originalProfilingValue;
}
});
it('fails every overlapping database request capture closed without serializing execution', async () => {
const activeCaptures = new Set<WorkerPerformanceCapture>();
const firstHarness = createFakeRuntime();
const secondHarness = createFakeRuntime();
const first = startWorkerPerformanceCapture({
enabled: true,
runtime: firstHarness.runtime,
});
const second = startWorkerPerformanceCapture({
enabled: true,
runtime: secondHarness.runtime,
});
registerDatabaseWorkerPerformanceCapture(activeCaptures, first);
registerDatabaseWorkerPerformanceCapture(activeCaptures, second);
await Promise.all([
armWorkerPerformanceCapture(first),
armWorkerPerformanceCapture(second),
]);
const [firstExecution, secondExecution] = await Promise.all([
executeWithWorkerPerformanceCapture(first, async () => 'first'),
executeWithWorkerPerformanceCapture(second, async () => 'second'),
]);
releaseDatabaseWorkerPerformanceCapture(activeCaptures, first);
releaseDatabaseWorkerPerformanceCapture(activeCaptures, second);
for (const execution of [firstExecution, secondExecution]) {
expect(execution.performance).toMatchObject({
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
WORKER_PERFORMANCE_INVALID_REASON.OVERLAPPING_DATABASE_WORKER_REQUESTS,
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
WORKER_PERFORMANCE_INVALID_REASON.OVERLAPPING_DATABASE_WORKER_REQUESTS,
histogramFlushedEpochMs: null,
invalidReason:
WORKER_PERFORMANCE_INVALID_REASON.OVERLAPPING_DATABASE_WORKER_REQUESTS,
threadCpuSystemMicros: null,
threadCpuUnavailableReason:
WORKER_PERFORMANCE_INVALID_REASON.OVERLAPPING_DATABASE_WORKER_REQUESTS,
threadCpuUserMicros: null,
});
}
expect(firstExecution.result).toBe('first');
expect(secondExecution.result).toBe('second');
expect(activeCaptures.size).toBe(0);
});
it('reliably observes a real 20ms event-loop block after condition-based arming', async () => {
process.env[PROFILING_ENV] = '1';
const capture = startWorkerPerformanceCapture();
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => {
const blockStartedAt = performance.now();
while (performance.now() - blockStartedAt < 20) {
// This deliberate block is the behavior under measurement.
}
}
);
expect(execution.success).toBe(true);
expect(execution.performance?.eventLoopDelay).not.toBeNull();
expect(execution.performance?.eventLoopDelay?.maxMs).toBeGreaterThan(
10
);
expect(execution.performance?.histogramFlushedEpochMs).not.toBeNull();
});
});
@@ -0,0 +1,39 @@
import {
armWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
startWorkerPerformanceCapture,
} from './worker-performance-capture';
import { createFakeRuntime } from './worker-performance-capture.test-harness';
describe('worker performance histogram lifecycle', () => {
it('accepts a nonthrowing false disable result because the histogram is already disabled', async () => {
const harness = createFakeRuntime({
histogramDisableResult: false,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'business-result'
);
expect(execution).toMatchObject({
error: null,
result: 'business-result',
success: true,
performance: {
eventLoopDelay: {
maxMs: 24,
p95Ms: 18,
p99Ms: 22,
},
eventLoopDelayUnavailableReason: null,
histogramFlushedEpochMs: 145,
},
});
});
});
@@ -0,0 +1,136 @@
import {
WORKER_PERFORMANCE_UNAVAILABLE_REASON,
type WorkerEventLoopDelayHistogram,
type WorkerEventLoopDelayMetrics,
type WorkerPerformanceCapture,
type WorkerPerformanceMetricUnavailableReason,
} from './worker-performance-capture.model';
import {
HISTOGRAM_POLL_INTERVAL_MS,
HISTOGRAM_WAIT_CAP_MS,
} from './worker-performance-capture.runtime';
export function disableWorkerPerformanceHistogram(
capture: WorkerPerformanceCapture
): boolean {
if (!capture.eventLoopDelay || capture.histogramDisabled) {
return true;
}
try {
capture.eventLoopDelay.disable();
capture.histogramDisabled = true;
return true;
} catch {
// Performance instrumentation must never affect worker execution.
return false;
}
}
export function markEventLoopDelayUnavailable(
capture: WorkerPerformanceCapture,
reason: WorkerPerformanceMetricUnavailableReason
): void {
capture.eventLoopDelayUnavailableReason ??= reason;
disableWorkerPerformanceHistogram(capture);
}
export async function waitForHistogramCondition(
capture: WorkerPerformanceCapture,
condition: (histogram: WorkerEventLoopDelayHistogram) => boolean
): Promise<boolean> {
const histogram = capture.eventLoopDelay;
if (
!histogram ||
capture.histogramDisabled ||
capture.invalidReason !== null
) {
return false;
}
let startedAt: number;
try {
startedAt = capture.runtime.readMonotonicMs();
} catch {
return false;
}
const maximumPolls = Math.ceil(
HISTOGRAM_WAIT_CAP_MS / HISTOGRAM_POLL_INTERVAL_MS
);
let pollCount = 0;
while (true) {
if (capture.invalidReason !== null || capture.histogramDisabled) {
return false;
}
try {
if (condition(histogram)) {
return true;
}
} catch {
return false;
}
let elapsedMs: number;
try {
elapsedMs = capture.runtime.readMonotonicMs() - startedAt;
} catch {
return false;
}
if (
!Number.isFinite(elapsedMs) ||
elapsedMs >= HISTOGRAM_WAIT_CAP_MS ||
pollCount >= maximumPolls
) {
return false;
}
const delayMs = Math.min(
HISTOGRAM_POLL_INTERVAL_MS,
HISTOGRAM_WAIT_CAP_MS - Math.max(0, elapsedMs)
);
try {
pollCount += 1;
await new Promise<void>((resolvePromise) => {
capture.runtime.scheduleTimeout(resolvePromise, delayMs);
});
} catch {
return false;
}
}
}
export function readWorkerEventLoopDelay(
capture: WorkerPerformanceCapture
): WorkerEventLoopDelayMetrics | null {
if (
capture.invalidReason !== null ||
capture.eventLoopDelayUnavailableReason !== null ||
capture.histogramFlushedEpochMs === null ||
!capture.eventLoopDelay
) {
return null;
}
try {
const metrics: WorkerEventLoopDelayMetrics = {
maxMs: capture.eventLoopDelay.max / 1e6,
p95Ms: capture.eventLoopDelay.percentile(95) / 1e6,
p99Ms: capture.eventLoopDelay.percentile(99) / 1e6,
};
if (
Object.values(metrics).every(
(value) => Number.isFinite(value) && value >= 0
)
) {
return metrics;
}
} catch {
// The fixed reason below is intentionally payload-free.
}
capture.eventLoopDelayUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_INVALID;
return null;
}
@@ -0,0 +1,235 @@
import type { EventLoopUtilization } from 'node:perf_hooks';
import {
WORKER_PERFORMANCE_UNAVAILABLE_REASON,
type WorkerPerformanceCapture,
type WorkerPerformanceCaptureResult,
} from './worker-performance-capture.model';
import { readWorkerEventLoopDelay } from './worker-performance-capture.histogram';
import { safeReadEpochMs } from './worker-performance-capture.runtime';
function readThreadCpuBoundary(
capture: WorkerPerformanceCapture
): NodeJS.CpuUsage | null {
if (!capture.runtime.readThreadCpuUsage) {
capture.threadCpuUnavailableReason ??=
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_UNAVAILABLE;
return null;
}
try {
const usage = capture.runtime.readThreadCpuUsage();
if (
!Number.isFinite(usage.system) ||
!Number.isFinite(usage.user) ||
usage.system < 0 ||
usage.user < 0
) {
capture.threadCpuUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_INVALID;
return null;
}
return usage;
} catch {
capture.threadCpuUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_UNAVAILABLE;
return null;
}
}
function readEventLoopUtilizationBoundary(
capture: WorkerPerformanceCapture
): EventLoopUtilization | null {
try {
const utilization = capture.runtime.readEventLoopUtilization();
if (
!Number.isFinite(utilization.active) ||
!Number.isFinite(utilization.idle)
) {
capture.eventLoopUtilizationUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_UTILIZATION_UNAVAILABLE;
return null;
}
return utilization;
} catch {
capture.eventLoopUtilizationUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_UTILIZATION_UNAVAILABLE;
return null;
}
}
export function beginWorkerPerformanceCapture(
capture: WorkerPerformanceCapture | null
): void {
if (!capture) {
return;
}
capture.workStartedEpochMs = safeReadEpochMs(capture.runtime);
capture.threadCpuStart = readThreadCpuBoundary(capture);
capture.eventLoopUtilizationStart =
readEventLoopUtilizationBoundary(capture);
}
function calculateThreadCpu(
capture: WorkerPerformanceCapture
): Pick<
WorkerPerformanceCaptureResult,
'threadCpuSystemMicros' | 'threadCpuUserMicros'
> {
if (
capture.invalidReason !== null ||
capture.threadCpuUnavailableReason !== null ||
!capture.threadCpuStart ||
!capture.threadCpuEnd
) {
return {
threadCpuSystemMicros: null,
threadCpuUserMicros: null,
};
}
try {
const system =
capture.threadCpuEnd.system - capture.threadCpuStart.system;
const user = capture.threadCpuEnd.user - capture.threadCpuStart.user;
if (
!Number.isFinite(system) ||
!Number.isFinite(user) ||
system < 0 ||
user < 0
) {
capture.threadCpuUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_INVALID;
return {
threadCpuSystemMicros: null,
threadCpuUserMicros: null,
};
}
return {
threadCpuSystemMicros: system,
threadCpuUserMicros: user,
};
} catch {
capture.threadCpuUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_INVALID;
return {
threadCpuSystemMicros: null,
threadCpuUserMicros: null,
};
}
}
function calculateEventLoopUtilization(
capture: WorkerPerformanceCapture
): number | null {
if (
capture.invalidReason !== null ||
capture.eventLoopUtilizationUnavailableReason !== null ||
!capture.eventLoopUtilizationStart ||
!capture.eventLoopUtilizationEnd
) {
return null;
}
try {
const active =
capture.eventLoopUtilizationEnd.active -
capture.eventLoopUtilizationStart.active;
const idle =
capture.eventLoopUtilizationEnd.idle -
capture.eventLoopUtilizationStart.idle;
const total = active + idle;
if (
!Number.isFinite(active) ||
!Number.isFinite(idle) ||
active < 0 ||
idle < 0 ||
total < 0
) {
capture.eventLoopUtilizationUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_UTILIZATION_UNAVAILABLE;
return null;
}
return total === 0 ? 0 : active / total;
} catch {
capture.eventLoopUtilizationUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_UTILIZATION_UNAVAILABLE;
return null;
}
}
function createFallbackResult(
capture: WorkerPerformanceCapture
): WorkerPerformanceCaptureResult {
const invalidReason = capture.invalidReason;
return {
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
invalidReason ??
capture.eventLoopDelayUnavailableReason ??
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE,
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
invalidReason ??
capture.eventLoopUtilizationUnavailableReason ??
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_UTILIZATION_UNAVAILABLE,
histogramFlushedEpochMs: null,
invalidReason,
requestReceivedEpochMs: capture.requestReceivedEpochMs,
threadCpuSystemMicros: null,
threadCpuUnavailableReason:
invalidReason ??
capture.threadCpuUnavailableReason ??
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_UNAVAILABLE,
threadCpuUserMicros: null,
workEndedEpochMs:
capture.workEndedEpochMs ?? capture.requestReceivedEpochMs,
workStartedEpochMs:
capture.workStartedEpochMs ?? capture.requestReceivedEpochMs,
};
}
export function createWorkerPerformanceCaptureResult(
capture: WorkerPerformanceCapture
): WorkerPerformanceCaptureResult {
try {
const overlapReason = capture.invalidReason;
if (overlapReason) {
capture.eventLoopDelayUnavailableReason = overlapReason;
capture.eventLoopUtilizationUnavailableReason = overlapReason;
capture.threadCpuUnavailableReason = overlapReason;
capture.histogramFlushedEpochMs = null;
}
const threadCpu = calculateThreadCpu(capture);
return {
eventLoopDelay: readWorkerEventLoopDelay(capture),
eventLoopDelayUnavailableReason:
capture.eventLoopDelayUnavailableReason,
eventLoopUtilization: calculateEventLoopUtilization(capture),
eventLoopUtilizationUnavailableReason:
capture.eventLoopUtilizationUnavailableReason,
histogramFlushedEpochMs: capture.histogramFlushedEpochMs,
invalidReason: capture.invalidReason,
requestReceivedEpochMs: capture.requestReceivedEpochMs,
...threadCpu,
threadCpuUnavailableReason: capture.threadCpuUnavailableReason,
workEndedEpochMs:
capture.workEndedEpochMs ?? capture.requestReceivedEpochMs,
workStartedEpochMs:
capture.workStartedEpochMs ?? capture.requestReceivedEpochMs,
};
} catch {
return createFallbackResult(capture);
}
}
export function finishWorkerPerformanceMetricBoundaries(
capture: WorkerPerformanceCapture
): void {
capture.workEndedEpochMs = safeReadEpochMs(capture.runtime);
capture.threadCpuEnd = readThreadCpuBoundary(capture);
capture.eventLoopUtilizationEnd = readEventLoopUtilizationBoundary(capture);
}
@@ -0,0 +1,94 @@
import type { EventLoopUtilization } from 'node:perf_hooks';
export const WORKER_PERFORMANCE_INVALID_REASON = {
OVERLAPPING_DATABASE_WORKER_REQUESTS:
'overlapping-database-worker-requests',
} as const;
export type WorkerPerformanceInvalidReason =
(typeof WORKER_PERFORMANCE_INVALID_REASON)[keyof typeof WORKER_PERFORMANCE_INVALID_REASON];
export const WORKER_PERFORMANCE_UNAVAILABLE_REASON = {
EVENT_LOOP_DELAY_ARM_TIMEOUT: 'event-loop-delay-arm-timeout',
EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE:
'event-loop-delay-capture-unavailable',
EVENT_LOOP_DELAY_FLUSH_TIMEOUT: 'event-loop-delay-flush-timeout',
EVENT_LOOP_DELAY_INVALID: 'event-loop-delay-invalid',
EVENT_LOOP_UTILIZATION_UNAVAILABLE: 'event-loop-utilization-unavailable',
THREAD_CPU_USAGE_INVALID: 'thread-cpu-usage-invalid',
THREAD_CPU_USAGE_UNAVAILABLE: 'thread-cpu-usage-unavailable',
} as const;
export type WorkerPerformanceUnavailableReason =
(typeof WORKER_PERFORMANCE_UNAVAILABLE_REASON)[keyof typeof WORKER_PERFORMANCE_UNAVAILABLE_REASON];
export type WorkerPerformanceMetricUnavailableReason =
WorkerPerformanceInvalidReason | WorkerPerformanceUnavailableReason;
export interface WorkerEventLoopDelayMetrics {
maxMs: number;
p95Ms: number;
p99Ms: number;
}
export interface WorkerEventLoopDelayHistogram {
readonly count: number;
readonly max: number;
disable: () => boolean;
enable: () => boolean;
percentile: (percentile: number) => number;
}
export interface WorkerPerformanceCaptureResult {
eventLoopDelay: WorkerEventLoopDelayMetrics | null;
eventLoopDelayUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
eventLoopUtilization: number | null;
eventLoopUtilizationUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
histogramFlushedEpochMs: number | null;
invalidReason: WorkerPerformanceInvalidReason | null;
requestReceivedEpochMs: number;
threadCpuSystemMicros: number | null;
threadCpuUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
threadCpuUserMicros: number | null;
workEndedEpochMs: number;
workStartedEpochMs: number;
}
export interface WorkerPerformanceCaptureRuntime {
createEventLoopDelayHistogram: () => WorkerEventLoopDelayHistogram;
readEpochMs: () => number;
readEventLoopUtilization: () => EventLoopUtilization;
readMonotonicMs: () => number;
readThreadCpuUsage: (() => NodeJS.CpuUsage) | null;
scheduleTimeout: (callback: () => void, delayMs: number) => void;
}
export interface WorkerPerformanceCaptureOptions {
enabled?: boolean;
runtime?: WorkerPerformanceCaptureRuntime;
}
export interface WorkerPerformanceExecutionResult<TResult> {
error: unknown | null;
performance: WorkerPerformanceCaptureResult | undefined;
result: TResult | undefined;
success: boolean;
}
export interface WorkerPerformanceCapture {
eventLoopDelay: WorkerEventLoopDelayHistogram | null;
eventLoopDelayUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
eventLoopUtilizationEnd: EventLoopUtilization | null;
eventLoopUtilizationStart: EventLoopUtilization | null;
eventLoopUtilizationUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
histogramDisabled: boolean;
histogramFlushedEpochMs: number | null;
invalidReason: WorkerPerformanceInvalidReason | null;
requestReceivedEpochMs: number;
runtime: WorkerPerformanceCaptureRuntime;
threadCpuEnd: NodeJS.CpuUsage | null;
threadCpuStart: NodeJS.CpuUsage | null;
threadCpuUnavailableReason: WorkerPerformanceMetricUnavailableReason | null;
workEndedEpochMs: number | null;
workStartedEpochMs: number | null;
}
@@ -0,0 +1,45 @@
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
import type {
WorkerPerformanceCaptureRuntime,
WorkerEventLoopDelayHistogram,
} from './worker-performance-capture.model';
export const WORKER_PROFILING_ENV = 'IPTVNATOR_PERF_WORKER_PROFILING';
export const HISTOGRAM_POLL_INTERVAL_MS = 1;
export const HISTOGRAM_WAIT_CAP_MS = 50;
function readRuntimeThreadCpuUsage(): (() => NodeJS.CpuUsage) | null {
const candidate: unknown = Reflect.get(process, 'threadCpuUsage');
if (typeof candidate !== 'function') {
return null;
}
return () =>
(candidate as (this: NodeJS.Process) => NodeJS.CpuUsage).call(process);
}
export const DEFAULT_WORKER_PERFORMANCE_RUNTIME: WorkerPerformanceCaptureRuntime =
{
createEventLoopDelayHistogram: (): WorkerEventLoopDelayHistogram =>
monitorEventLoopDelay({
resolution: HISTOGRAM_POLL_INTERVAL_MS,
}),
readEpochMs: () => performance.timeOrigin + performance.now(),
readEventLoopUtilization: () => performance.eventLoopUtilization(),
readMonotonicMs: () => performance.now(),
readThreadCpuUsage: readRuntimeThreadCpuUsage(),
scheduleTimeout: (callback, delayMs) => {
setTimeout(callback, delayMs);
},
};
export function safeReadEpochMs(
runtime: WorkerPerformanceCaptureRuntime
): number {
try {
const epochMs = runtime.readEpochMs();
return Number.isFinite(epochMs) ? epochMs : Date.now();
} catch {
return Date.now();
}
}
@@ -1,8 +1,10 @@
import {
WORKER_PERFORMANCE_UNAVAILABLE_REASON,
armWorkerPerformanceCapture,
finishWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
startWorkerPerformanceCapture,
} from './worker-performance-capture';
import { createFakeRuntime } from './worker-performance-capture.test-harness';
const PROFILING_ENV = 'IPTVNATOR_PERF_WORKER_PROFILING';
@@ -23,26 +25,355 @@ describe('worker performance capture', () => {
expect(startWorkerPerformanceCapture()).toBeNull();
});
it('reports finite worker event-loop metrics when opted in', async () => {
process.env[PROFILING_ENV] = '1';
const capture = startWorkerPerformanceCapture();
await armWorkerPerformanceCapture(capture);
const blockStartedAt = performance.now();
while (performance.now() - blockStartedAt < 20) {
// Deliberately block the loop so the histogram contract is tested.
}
const result = await finishWorkerPerformanceCapture(capture);
expect(result).toEqual({
eventLoopDelay: {
maxMs: expect.any(Number),
p95Ms: expect.any(Number),
p99Ms: expect.any(Number),
},
eventLoopUtilization: expect.any(Number),
it('captures exact work-boundary CPU, ELU, timestamps, and a flushed fresh histogram', async () => {
const harness = createFakeRuntime();
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => {
harness.lifecycle.push('execute');
await Promise.resolve();
harness.lifecycle.push('execute:settled');
return 'result';
}
);
expect(execution).toEqual({
error: null,
performance: {
eventLoopDelay: {
maxMs: 24,
p95Ms: 18,
p99Ms: 22,
},
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: 0.75,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 145,
invalidReason: null,
requestReceivedEpochMs: 100,
threadCpuSystemMicros: 30,
threadCpuUnavailableReason: null,
threadCpuUserMicros: 80,
workEndedEpochMs: 140,
workStartedEpochMs: 110,
},
result: 'result',
success: true,
});
expect(harness.lifecycle).toEqual([
'epoch:100',
'histogram:create',
'timeout:1',
'epoch:110',
'cpu:1',
'elu:1',
'execute',
'execute:settled',
'epoch:140',
'cpu:2',
'elu:2',
'timeout:1',
'epoch:145',
]);
expect(harness.histogramLifecycle.enableCalls).toBe(1);
expect(harness.histogramLifecycle.disableCalls).toBe(1);
expect('reset' in harness.histogram).toBe(false);
});
it('finishes profiling before returning a rejected execution to worker side effects', async () => {
const harness = createFakeRuntime();
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
const failure = new Error('worker failed');
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => {
harness.lifecycle.push('execute');
await Promise.resolve();
harness.lifecycle.push('execute:rejected');
throw failure;
}
);
harness.lifecycle.push('worker:emit-error');
expect(execution).toMatchObject({
error: failure,
success: false,
});
expect(harness.lifecycle.indexOf('epoch:140')).toBe(
harness.lifecycle.indexOf('execute:rejected') + 1
);
expect(harness.lifecycle.indexOf('worker:emit-error')).toBeGreaterThan(
harness.lifecycle.indexOf('epoch:145')
);
});
it('times out arming after a monotonic 50ms without blocking work metrics', async () => {
const harness = createFakeRuntime({
armHistogram: false,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'result'
);
expect(
harness.scheduledTimeouts.reduce(
(total, timeout) => total + timeout,
0
)
).toBe(50);
expect(execution.performance).toMatchObject({
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_ARM_TIMEOUT,
eventLoopUtilization: 0.75,
histogramFlushedEpochMs: null,
threadCpuSystemMicros: 30,
threadCpuUserMicros: 80,
});
});
it('cannot poll forever when the monotonic runtime clock stalls', async () => {
const harness = createFakeRuntime({
armHistogram: false,
stallMonotonicClock: true,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'result'
);
expect(harness.scheduledTimeouts).toHaveLength(50);
expect(execution).toMatchObject({
result: 'result',
success: true,
performance: {
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_ARM_TIMEOUT,
},
});
});
it('times out flushing after a monotonic 50ms while preserving work CPU and ELU', async () => {
const harness = createFakeRuntime({
flushHistogram: false,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'result'
);
expect(
harness.scheduledTimeouts
.slice(1)
.reduce((total, timeout) => total + timeout, 0)
).toBe(50);
expect(execution.performance).toMatchObject({
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_FLUSH_TIMEOUT,
eventLoopUtilization: 0.75,
histogramFlushedEpochMs: null,
threadCpuSystemMicros: 30,
threadCpuUserMicros: 80,
});
});
it('reports unavailable thread CPU without falling back to process CPU usage', async () => {
const harness = createFakeRuntime({
threadCpuAvailable: false,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'result'
);
expect(execution.performance).toMatchObject({
eventLoopUtilization: 0.75,
threadCpuSystemMicros: null,
threadCpuUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_UNAVAILABLE,
threadCpuUserMicros: null,
});
expect(harness.lifecycle).not.toContain('cpu:1');
expect(harness.lifecycle).not.toContain('cpu:2');
});
it('preserves the business result when CPU and ELU runtime callbacks throw', async () => {
const harness = createFakeRuntime({
throwBoundaryCallbacks: true,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'business-result'
);
expect(execution).toMatchObject({
error: null,
result: 'business-result',
success: true,
performance: {
eventLoopDelay: {
maxMs: 24,
p95Ms: 18,
p99Ms: 22,
},
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_UTILIZATION_UNAVAILABLE,
threadCpuSystemMicros: null,
threadCpuUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_UNAVAILABLE,
threadCpuUserMicros: null,
},
});
});
it('preserves work metrics and the business result when timer scheduling throws', async () => {
const harness = createFakeRuntime({
throwScheduleTimeout: true,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'business-result'
);
expect(execution).toMatchObject({
error: null,
result: 'business-result',
success: true,
performance: {
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_ARM_TIMEOUT,
eventLoopUtilization: 0.75,
threadCpuSystemMicros: 30,
threadCpuUserMicros: 80,
},
});
});
it.each([
{
label: 'count getter during finish',
options: {
throwHistogramCountAtRead: 3,
},
reason: WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE,
},
{
label: 'metric getter after flush',
options: {
throwHistogramMetric: true,
},
reason: WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_INVALID,
},
])(
'preserves the business result when the histogram $label throws',
async ({ options, reason }) => {
const harness = createFakeRuntime(options);
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'business-result'
);
expect(execution).toMatchObject({
error: null,
result: 'business-result',
success: true,
performance: {
eventLoopDelay: null,
eventLoopDelayUnavailableReason: reason,
eventLoopUtilization: 0.75,
threadCpuSystemMicros: 30,
threadCpuUserMicros: 80,
},
});
}
);
it('keeps the business result and rejects delay metrics when histogram disable throws', async () => {
const harness = createFakeRuntime({
throwHistogramDisable: true,
});
const capture = startWorkerPerformanceCapture({
enabled: true,
runtime: harness.runtime,
});
await armWorkerPerformanceCapture(capture);
const execution = await executeWithWorkerPerformanceCapture(
capture,
async () => 'business-result'
);
expect(execution).toMatchObject({
error: null,
result: 'business-result',
success: true,
performance: {
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE,
eventLoopUtilization: 0.75,
histogramFlushedEpochMs: null,
threadCpuSystemMicros: 30,
threadCpuUserMicros: 80,
},
});
expect(result?.eventLoopUtilization).toBeGreaterThanOrEqual(0);
expect(result?.eventLoopDelay.maxMs).toBeGreaterThan(10);
});
});
@@ -0,0 +1,158 @@
import type {
WorkerEventLoopDelayHistogram,
WorkerPerformanceCaptureRuntime,
} from './worker-performance-capture';
interface FakeRuntimeOptions {
armHistogram?: boolean;
flushHistogram?: boolean;
histogramDisableResult?: boolean;
stallMonotonicClock?: boolean;
threadCpuAvailable?: boolean;
throwBoundaryCallbacks?: boolean;
throwHistogramDisable?: boolean;
throwHistogramCountAtRead?: number;
throwHistogramMetric?: boolean;
throwScheduleTimeout?: boolean;
}
export interface FakeRuntimeHarness {
histogram: WorkerEventLoopDelayHistogram;
histogramLifecycle: {
disableCalls: number;
enableCalls: number;
};
lifecycle: string[];
runtime: WorkerPerformanceCaptureRuntime;
scheduledTimeouts: number[];
}
export function createFakeRuntime(
options: FakeRuntimeOptions = {}
): FakeRuntimeHarness {
const lifecycle: string[] = [];
const scheduledTimeouts: number[] = [];
let epochIndex = 0;
let monotonicMs = 0;
let timeoutCount = 0;
let cpuIndex = 0;
let eluIndex = 0;
let histogramCount = 0;
let histogramCountReads = 0;
const histogramLifecycle = {
disableCalls: 0,
enableCalls: 0,
};
const epochValues = [100, 110, 140, 145];
const cpuValues = [
{ system: 50, user: 100 },
{ system: 80, user: 180 },
];
const eluValues = [
{ active: 10, idle: 20, utilization: 1 / 3 },
{ active: 40, idle: 30, utilization: 4 / 7 },
];
const histogram = {
get count() {
histogramCountReads += 1;
if (histogramCountReads === options.throwHistogramCountAtRead) {
throw new Error('histogram count unavailable');
}
return histogramCount;
},
disable: () => {
histogramLifecycle.disableCalls += 1;
if (options.throwHistogramDisable === true) {
throw new Error('histogram disable unavailable');
}
return options.histogramDisableResult ?? true;
},
enable: () => {
histogramLifecycle.enableCalls += 1;
return true;
},
get max() {
if (options.throwHistogramMetric === true) {
throw new Error('histogram max unavailable');
}
return 24_000_000;
},
percentile: (percentile: number) => {
if (options.throwHistogramMetric === true) {
throw new Error('histogram percentile unavailable');
}
if (percentile === 95) {
return 18_000_000;
}
if (percentile === 99) {
return 22_000_000;
}
return 0;
},
} satisfies WorkerEventLoopDelayHistogram;
const runtime: WorkerPerformanceCaptureRuntime = {
createEventLoopDelayHistogram: () => {
lifecycle.push('histogram:create');
return histogram;
},
readEpochMs: () => {
const value = epochValues[epochIndex] ?? 145 + epochIndex;
epochIndex += 1;
lifecycle.push(`epoch:${value}`);
return value;
},
readEventLoopUtilization: () => {
if (options.throwBoundaryCallbacks === true) {
throw new Error('ELU callback unavailable');
}
const value = eluValues[eluIndex] ?? eluValues.at(-1)!;
eluIndex += 1;
lifecycle.push(`elu:${eluIndex}`);
return value;
},
readMonotonicMs: () => monotonicMs,
readThreadCpuUsage:
options.threadCpuAvailable === false
? null
: () => {
if (options.throwBoundaryCallbacks === true) {
throw new Error('thread CPU callback unavailable');
}
const value = cpuValues[cpuIndex] ?? cpuValues.at(-1)!;
cpuIndex += 1;
lifecycle.push(`cpu:${cpuIndex}`);
return value;
},
scheduleTimeout: (callback, delayMs) => {
lifecycle.push(`timeout:${delayMs}`);
scheduledTimeouts.push(delayMs);
if (options.throwScheduleTimeout === true) {
throw new Error('timer callback unavailable');
}
if (options.stallMonotonicClock !== true) {
monotonicMs += delayMs;
} else if (timeoutCount >= 55) {
throw new Error('test scheduler fail-safe');
}
timeoutCount += 1;
if (timeoutCount === 1 && options.armHistogram !== false) {
histogramCount += 1;
} else if (
timeoutCount > 1 &&
options.armHistogram !== false &&
options.flushHistogram !== false
) {
histogramCount += 1;
}
callback();
},
};
return {
histogram,
histogramLifecycle,
lifecycle,
runtime,
scheduledTimeouts,
};
}
@@ -1,47 +1,119 @@
import {
monitorEventLoopDelay,
performance,
type IntervalHistogram,
} from 'node:perf_hooks';
WORKER_PERFORMANCE_INVALID_REASON,
WORKER_PERFORMANCE_UNAVAILABLE_REASON,
type WorkerPerformanceCapture,
type WorkerPerformanceCaptureOptions,
type WorkerPerformanceCaptureResult,
type WorkerPerformanceExecutionResult,
} from './worker-performance-capture.model';
import {
disableWorkerPerformanceHistogram,
markEventLoopDelayUnavailable,
waitForHistogramCondition,
} from './worker-performance-capture.histogram';
import {
beginWorkerPerformanceCapture,
createWorkerPerformanceCaptureResult,
finishWorkerPerformanceMetricBoundaries,
} from './worker-performance-capture.metrics';
import {
DEFAULT_WORKER_PERFORMANCE_RUNTIME,
safeReadEpochMs,
WORKER_PROFILING_ENV,
} from './worker-performance-capture.runtime';
const WORKER_PROFILING_ENV = 'IPTVNATOR_PERF_WORKER_PROFILING';
export {
WORKER_PERFORMANCE_INVALID_REASON,
WORKER_PERFORMANCE_UNAVAILABLE_REASON,
} from './worker-performance-capture.model';
export type {
WorkerEventLoopDelayHistogram,
WorkerEventLoopDelayMetrics,
WorkerPerformanceCapture,
WorkerPerformanceCaptureOptions,
WorkerPerformanceCaptureResult,
WorkerPerformanceCaptureRuntime,
WorkerPerformanceExecutionResult,
WorkerPerformanceInvalidReason,
WorkerPerformanceUnavailableReason,
} from './worker-performance-capture.model';
export interface WorkerPerformanceCaptureResult {
eventLoopDelay: {
maxMs: number;
p95Ms: number;
p99Ms: number;
};
eventLoopUtilization: number;
}
interface WorkerPerformanceCapture {
eventLoopDelay: IntervalHistogram;
eventLoopUtilizationStart: ReturnType<
typeof performance.eventLoopUtilization
>;
}
export function startWorkerPerformanceCapture(): WorkerPerformanceCapture | null {
if (process.env[WORKER_PROFILING_ENV] !== '1') {
export function startWorkerPerformanceCapture(
options: WorkerPerformanceCaptureOptions = {}
): WorkerPerformanceCapture | null {
const enabled =
options.enabled ?? process.env[WORKER_PROFILING_ENV] === '1';
if (!enabled) {
return null;
}
const eventLoopDelay = monitorEventLoopDelay({ resolution: 1 });
eventLoopDelay.enable();
return {
eventLoopDelay,
eventLoopUtilizationStart: performance.eventLoopUtilization(),
const runtime = options.runtime ?? DEFAULT_WORKER_PERFORMANCE_RUNTIME;
const capture: WorkerPerformanceCapture = {
eventLoopDelay: null,
eventLoopDelayUnavailableReason: null,
eventLoopUtilizationEnd: null,
eventLoopUtilizationStart: null,
eventLoopUtilizationUnavailableReason: null,
histogramDisabled: false,
histogramFlushedEpochMs: null,
invalidReason: null,
requestReceivedEpochMs: safeReadEpochMs(runtime),
runtime,
threadCpuEnd: null,
threadCpuStart: null,
threadCpuUnavailableReason:
runtime.readThreadCpuUsage === null
? WORKER_PERFORMANCE_UNAVAILABLE_REASON.THREAD_CPU_USAGE_UNAVAILABLE
: null,
workEndedEpochMs: null,
workStartedEpochMs: null,
};
try {
capture.eventLoopDelay = runtime.createEventLoopDelayHistogram();
capture.eventLoopDelay.enable();
} catch {
capture.eventLoopDelay = null;
capture.eventLoopDelayUnavailableReason =
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE;
}
return capture;
}
export async function armWorkerPerformanceCapture(
capture: WorkerPerformanceCapture | null
): Promise<void> {
if (capture) {
await new Promise<void>((resolvePromise) =>
setTimeout(resolvePromise, 2)
if (!capture) {
return;
}
try {
if (
capture.invalidReason !== null ||
capture.eventLoopDelayUnavailableReason !== null
) {
return;
}
const armed = await waitForHistogramCondition(
capture,
(histogram) => histogram.count > 0
);
if (!armed && capture.invalidReason === null) {
markEventLoopDelayUnavailable(
capture,
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_ARM_TIMEOUT
);
}
} catch {
try {
markEventLoopDelayUnavailable(
capture,
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE
);
} catch {
// Instrumentation must never prevent business execution.
}
}
}
@@ -52,17 +124,148 @@ export async function finishWorkerPerformanceCapture(
return undefined;
}
await new Promise<void>((resolvePromise) => setTimeout(resolvePromise, 2));
capture.eventLoopDelay.disable();
const eventLoopUtilization = performance.eventLoopUtilization(
capture.eventLoopUtilizationStart
finishWorkerPerformanceMetricBoundaries(capture);
if (
capture.invalidReason === null &&
capture.eventLoopDelayUnavailableReason === null &&
capture.eventLoopDelay
) {
try {
const workEndedHistogramCount = capture.eventLoopDelay.count;
const flushed = await waitForHistogramCondition(
capture,
(histogram) => histogram.count > workEndedHistogramCount
);
if (flushed) {
const disabled = disableWorkerPerformanceHistogram(capture);
if (disabled) {
capture.histogramFlushedEpochMs = safeReadEpochMs(
capture.runtime
);
} else {
markEventLoopDelayUnavailable(
capture,
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE
);
}
} else if (
capture.invalidReason === null &&
capture.eventLoopDelayUnavailableReason === null
) {
markEventLoopDelayUnavailable(
capture,
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_FLUSH_TIMEOUT
);
}
} catch {
markEventLoopDelayUnavailable(
capture,
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE
);
}
} else {
disableWorkerPerformanceHistogram(capture);
}
return createWorkerPerformanceCaptureResult(capture);
}
export async function executeWithWorkerPerformanceCapture<TResult>(
capture: WorkerPerformanceCapture | null,
execute: () => Promise<TResult>,
finish: (
capture: WorkerPerformanceCapture | null
) => Promise<
WorkerPerformanceCaptureResult | undefined
> = finishWorkerPerformanceCapture
): Promise<WorkerPerformanceExecutionResult<TResult>> {
try {
beginWorkerPerformanceCapture(capture);
} catch {
// Instrumentation setup must never prevent business execution.
}
let result: TResult;
try {
result = await execute();
} catch (error) {
const performance = await safelyFinishWorkerPerformanceCapture(
capture,
finish
);
return {
error,
performance,
result: undefined,
success: false,
};
}
const performance = await safelyFinishWorkerPerformanceCapture(
capture,
finish
);
return {
eventLoopDelay: {
maxMs: capture.eventLoopDelay.max / 1e6,
p95Ms: capture.eventLoopDelay.percentile(95) / 1e6,
p99Ms: capture.eventLoopDelay.percentile(99) / 1e6,
},
eventLoopUtilization: eventLoopUtilization.utilization,
error: null,
performance,
result,
success: true,
};
}
async function safelyFinishWorkerPerformanceCapture(
capture: WorkerPerformanceCapture | null,
finish: (
capture: WorkerPerformanceCapture | null
) => Promise<WorkerPerformanceCaptureResult | undefined>
): Promise<WorkerPerformanceCaptureResult | undefined> {
try {
return await finish(capture);
} catch {
if (!capture) {
return undefined;
}
try {
if (capture.workEndedEpochMs === null) {
finishWorkerPerformanceMetricBoundaries(capture);
}
markEventLoopDelayUnavailable(
capture,
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE
);
return createWorkerPerformanceCaptureResult(capture);
} catch {
return undefined;
}
}
}
export function registerDatabaseWorkerPerformanceCapture(
activeCaptures: Set<WorkerPerformanceCapture>,
capture: WorkerPerformanceCapture | null
): void {
if (!capture) {
return;
}
if (activeCaptures.size > 0) {
for (const activeCapture of activeCaptures) {
activeCapture.invalidReason =
WORKER_PERFORMANCE_INVALID_REASON.OVERLAPPING_DATABASE_WORKER_REQUESTS;
disableWorkerPerformanceHistogram(activeCapture);
}
capture.invalidReason =
WORKER_PERFORMANCE_INVALID_REASON.OVERLAPPING_DATABASE_WORKER_REQUESTS;
disableWorkerPerformanceHistogram(capture);
}
activeCaptures.add(capture);
}
export function releaseDatabaseWorkerPerformanceCapture(
activeCaptures: Set<WorkerPerformanceCapture>,
capture: WorkerPerformanceCapture | null
): void {
if (capture) {
activeCaptures.delete(capture);
}
}
@@ -0,0 +1,115 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import {
WORKER_PERFORMANCE_UNAVAILABLE_REASON,
armWorkerPerformanceCapture,
executeWithWorkerPerformanceCapture,
startWorkerPerformanceCapture,
} from './worker-performance-capture';
import { createFakeRuntime } from './worker-performance-capture.test-harness';
function readWorkerSource(filename: string): string {
return readFileSync(
join(process.cwd(), 'apps/electron-backend/src/app/workers', filename),
'utf8'
);
}
describe('worker performance integration order', () => {
it('profiles overlapping database requests without serializing their execution', () => {
const source = readWorkerSource('database.worker.ts');
const handler = source.slice(
source.lastIndexOf("parentPort.on('message'")
);
expect(handler).toContain('registerDatabaseWorkerPerformanceCapture');
expect(handler).toContain('executeWithWorkerPerformanceCapture');
expect(handler).toContain('releaseDatabaseWorkerPerformanceCapture');
expect(handler).toContain('finally');
expect(handler).not.toContain('finishWorkerPerformanceCapture');
expect(
handler.indexOf('executeWithWorkerPerformanceCapture')
).toBeLessThan(handler.indexOf('console.error'));
expect(
handler.indexOf('executeWithWorkerPerformanceCapture')
).toBeLessThan(handler.indexOf('postMessage'));
expect(handler).not.toContain('await previous');
expect(handler).not.toContain('requestQueue');
});
it('keeps playlist response serialization after profiling completion', () => {
const source = readWorkerSource('playlist-refresh.worker.ts');
const handler = source.slice(source.lastIndexOf('parentPort.on('));
const executionIndex = handler.indexOf(
'executeWithWorkerPerformanceCapture'
);
expect(executionIndex).toBeGreaterThanOrEqual(0);
expect(handler).not.toContain('finishWorkerPerformanceCapture');
expect(executionIndex).toBeLessThan(
handler.indexOf('serializeError', executionIndex)
);
expect(executionIndex).toBeLessThan(
handler.indexOf('postMessage', executionIndex)
);
});
it('preserves success and business errors when the finish seam throws once', async () => {
const successHarness = createFakeRuntime();
const successCapture = startWorkerPerformanceCapture({
enabled: true,
runtime: successHarness.runtime,
});
await armWorkerPerformanceCapture(successCapture);
const failingSuccessFinish = jest.fn(async () => {
throw new Error('finish failed');
});
const success = await executeWithWorkerPerformanceCapture(
successCapture,
async () => 'business-result',
failingSuccessFinish
);
expect(success).toMatchObject({
error: null,
result: 'business-result',
success: true,
performance: {
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
WORKER_PERFORMANCE_UNAVAILABLE_REASON.EVENT_LOOP_DELAY_CAPTURE_UNAVAILABLE,
eventLoopUtilization: 0.75,
threadCpuSystemMicros: 30,
threadCpuUserMicros: 80,
},
});
expect(failingSuccessFinish).toHaveBeenCalledTimes(1);
const errorHarness = createFakeRuntime();
const errorCapture = startWorkerPerformanceCapture({
enabled: true,
runtime: errorHarness.runtime,
});
await armWorkerPerformanceCapture(errorCapture);
const businessError = new Error('business failed');
const failingErrorFinish = jest.fn(async () => {
throw new Error('finish failed');
});
const failure = await executeWithWorkerPerformanceCapture(
errorCapture,
async () => {
throw businessError;
},
failingErrorFinish
);
expect(failure).toMatchObject({
error: businessError,
result: undefined,
success: false,
});
expect(failingErrorFinish).toHaveBeenCalledTimes(1);
});
});
+11
View File
@@ -49,6 +49,11 @@
],
"outputHashing": "all"
},
"electron-performance": {
"optimization": true,
"outputHashing": "all",
"sourceMap": true
},
"development": {
"optimization": false,
"extractLicenses": false,
@@ -63,6 +68,12 @@
"command": "pnpm nx run remote-control-web:build:development"
}
},
"build-performance": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm nx run remote-control-web:build:electron-performance"
}
},
"serve": {
"continuous": true,
"executor": "@angular/build:dev-server",
+28 -1
View File
@@ -13,7 +13,7 @@
"targets": {
"build": {
"executor": "@angular/build:application",
"outputs": ["{options.outputPath}"],
"outputs": ["{workspaceRoot}/dist/apps/web"],
"options": {
"outputPath": {
"base": "dist/apps/web",
@@ -122,6 +122,27 @@
"extractLicenses": false,
"sourceMap": true
},
"electron-performance": {
"baseHref": "./",
"serviceWorker": false,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false,
"removeSpecialComments": true
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": true,
"fileReplacements": [
{
"replace": "apps/web/src/environments/environment.ts",
"with": "apps/web/src/environments/environment.prod.ts"
}
]
},
"electron-e2e": {
"baseHref": "./",
"serviceWorker": false,
@@ -138,6 +159,12 @@
"command": "pnpm nx run web:build:electron-e2e"
}
},
"build-performance": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm nx run web:build:electron-performance"
}
},
"serve": {
"continuous": true,
"executor": "@angular/build:dev-server",
+23 -5
View File
@@ -121,11 +121,29 @@ before/after claims.
The target reserves and verifies CDP port 9222, freezes renderer long-task,
frame-gap, and heartbeat probes before forced post-GC heap collection, and
enables opt-in worker profiling. Worker event-loop delay is read from a
request-scoped `node:perf_hooks` capture; a worker terminated before it can flush
the capture reports the metric as unavailable rather than zero. Diagnostic CPU
profiles, heap snapshots, and Chromium traces are excluded from the five-run
headline distributions.
builds the Electron main process, renderer, and workers with optimized,
source-mapped performance configurations before enabling opt-in worker
profiling. Each worker response retains a raw
request-scoped record containing request/operation identity, received/work/flush
timestamps, thread CPU, event-loop utilization, event-loop delay, and fixed
unavailability or invalid reasons. Missing or malformed profiling metadata
still produces an outcome with its request identity, nullable metrics, and a
fixed capture-unavailable reason. A worker terminated before it can flush the
capture reports the metric as unavailable rather than zero.
The harness excludes workers created by pre-capture seed setup through an exact
capture-generation marker. Since the production M3U database payload
deliberately omits the refresh operation ID, the benchmark correlates only a
valid preload `start` marker to the exact database operation and playlist. A
missing or ambiguous marker leaves the raw operation ID `null` with a fixed
reason; it does not infer identity from timing alone.
Headline worker fields named `*WorkerRequest*` are distributions of individual
request metrics. In particular, request p95/p99 distributions are not presented
as an operation-wide or process-wide percentile, and database request
percentiles are never collapsed with `Math.max`. Diagnostic CPU profiles, heap
snapshots, and Chromium traces are excluded from the five-run headline
distributions.
### Reporting The Auto-Update Result
+29
View File
@@ -118,6 +118,35 @@ The worker contract lives in
3. `DbWorkerEventMessage`
4. `DbOperationEvent`
### Opt-in request performance capture
`IPTVNATOR_PERF_WORKER_PROFILING=1` adds development/test-only performance
metadata to each database-worker response. It is disabled by default and must
stay disabled for production launches.
Each enabled request gets a fresh event-loop-delay histogram and records:
- `requestReceivedEpochMs`, `workStartedEpochMs`, `workEndedEpochMs`, and
`histogramFlushedEpochMs`
- 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
- fixed invalid or unavailable reasons whenever a metric cannot be attributed
Histogram arming waits until the histogram has a sample; flushing waits for its
sample count to advance after work ends. Both waits use condition-based timer
polling. Each wait stops after 50 ms of observed monotonic time or its bounded
poll count; arming and flushing have separate caps, and timer scheduling may
overshoot wall-clock time. A timeout or profiling API failure never replaces
the business response: timestamps and any independently available CPU/ELU
metrics remain valid, while event-loop delay is `null` with a fixed reason.
The long-lived database worker still executes concurrent requests without a
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.
### Progress event contract
The worker now emits request-scoped events with:
+11 -1
View File
@@ -1,5 +1,10 @@
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Channel, EpgProgram, Playlist, PlaylistMeta } from '@iptvnator/shared/interfaces';
import {
Channel,
EpgProgram,
Playlist,
PlaylistMeta,
} from '@iptvnator/shared/interfaces';
export const PlaylistActions = createActionGroup({
source: 'Playlists',
@@ -11,6 +16,11 @@ export const PlaylistActions = createActionGroup({
'Remove Playlist': props<{ playlistId: string }>(),
'Update Playlist Meta': props<{ playlist: PlaylistMeta }>(),
'Update Playlist': props<{
/**
* Instrumentation-only; stripped before the DB invoke.
* Never enters the DB worker payload or persisted playlist data.
*/
operationId?: string;
playlist: Playlist;
playlistId: string;
refreshEpg?: boolean;
+25 -21
View File
@@ -55,6 +55,7 @@ import {
} from './external-player-payload.util';
import { resolvePlaylistScopedEpgFetchPlan } from './playlist-scoped-epg-fetch.util';
import { resolveActiveEpgProgramAction } from './resolve-active-epg-program.util';
import { persistPlaylistUpdate } from './playlist-update.effect-handler';
@Injectable({ providedIn: 'any' })
export class PlaylistEffects {
@@ -305,10 +306,7 @@ export class PlaylistEffects {
return this.actions$.pipe(
ofType(PlaylistActions.updatePlaylist),
switchMap((action) =>
this.playlistsService.updatePlaylist(action.playlistId, {
...action.playlist,
_id: action.playlistId,
}).pipe(
persistPlaylistUpdate(this.playlistsService, action).pipe(
tap(() => {
this.fetchPlaylistScopedEpg(action.playlist, {
force: action.refreshEpg === true,
@@ -353,12 +351,14 @@ export class PlaylistEffects {
if ('isTemporary' in action && action.isTemporary) {
return EMPTY;
}
return this.playlistsService.addPlaylist(action.playlist).pipe(
tap(() => {
this.fetchPlaylistScopedEpg(action.playlist);
this.navigateToPlaylist(action.playlist);
})
);
return this.playlistsService
.addPlaylist(action.playlist)
.pipe(
tap(() => {
this.fetchPlaylistScopedEpg(action.playlist);
this.navigateToPlaylist(action.playlist);
})
);
})
);
},
@@ -370,17 +370,21 @@ export class PlaylistEffects {
return this.actions$.pipe(
ofType(PlaylistActions.updatePlaylistMeta),
switchMap((action) =>
this.playlistsService.updatePlaylistMeta(action.playlist).pipe(
tap(() => {
if (
this.hasPlaylistScopedEpgSourceChange(
action.playlist
)
) {
this.fetchPlaylistScopedEpg(action.playlist);
}
})
)
this.playlistsService
.updatePlaylistMeta(action.playlist)
.pipe(
tap(() => {
if (
this.hasPlaylistScopedEpgSourceChange(
action.playlist
)
) {
this.fetchPlaylistScopedEpg(
action.playlist
);
}
})
)
)
);
},
@@ -0,0 +1,30 @@
import { type Playlist } from '@iptvnator/shared/interfaces';
import { of } from 'rxjs';
import { PlaylistActions } from './actions';
import { persistPlaylistUpdate } from './playlist-update.effect-handler';
describe('persistPlaylistUpdate', () => {
it('forwards the refresh operation ID to playlist persistence', () => {
const playlistsService = {
updatePlaylist: jest.fn(() => of(undefined)),
};
const playlist = {
_id: 'playlist-1',
playlist: { items: [] },
} as unknown as Playlist;
const action = PlaylistActions.updatePlaylist({
playlist,
playlistId: playlist._id,
refreshEpg: true,
operationId: 'playlist-refresh-operation',
});
persistPlaylistUpdate(playlistsService, action);
expect(playlistsService.updatePlaylist).toHaveBeenCalledWith(
playlist._id,
playlist,
'playlist-refresh-operation'
);
});
});
@@ -0,0 +1,29 @@
import type { Playlist } from '@iptvnator/shared/interfaces';
interface PlaylistUpdateAction {
operationId?: string;
playlist: Playlist;
playlistId: string;
}
interface PlaylistUpdater<TResult> {
updatePlaylist: (
playlistId: string,
playlist: Playlist,
operationId?: string
) => TResult;
}
export function persistPlaylistUpdate<TResult>(
playlistsService: PlaylistUpdater<TResult>,
action: PlaylistUpdateAction
): TResult {
return playlistsService.updatePlaylist(
action.playlistId,
{
...action.playlist,
_id: action.playlistId,
},
action.operationId
);
}
@@ -630,6 +630,7 @@ describe('PlaylistRefreshActionService', () => {
playlist: refreshedPlaylist,
playlistId: item._id,
refreshEpg: true,
operationId: 'playlist-refresh-op',
})
);
});
@@ -205,12 +205,11 @@ export class PlaylistRefreshActionService {
}
try {
const operationId =
this.databaseService.createOperationId('playlist-refresh');
const refreshedPlaylist =
await this.playlistRefreshService.refreshPlaylist({
operationId:
this.databaseService.createOperationId(
'playlist-refresh'
),
operationId,
playlistId: item._id,
title: item.title,
url: item.url,
@@ -228,6 +227,7 @@ export class PlaylistRefreshActionService {
},
playlistId: item._id,
refreshEpg: true,
operationId,
})
);
@@ -549,5 +549,15 @@ describe('RecentPlaylistsComponent busy state', () => {
expect(dataService.sendIpcEvent).not.toHaveBeenCalled();
await Promise.resolve();
expect(store.dispatch).toHaveBeenCalledWith(
PlaylistActions.updatePlaylist({
playlist: expect.objectContaining({
_id: item._id,
}),
playlistId: item._id,
refreshEpg: true,
operationId: 'playlist-refresh-op',
})
);
});
});
@@ -463,6 +463,7 @@ export class RecentPlaylistsComponent {
},
playlistId: item._id,
refreshEpg: true,
operationId,
})
);
@@ -1336,6 +1336,37 @@ describe('PlaylistsService', () => {
expect(electron.dbUpsertAppPlaylist).toHaveBeenCalledTimes(2);
});
it('forwards refresh instrumentation only to its SQLite read and write', async () => {
const { electron } = createStatefulElectronStore(
createBasePlaylist('playlist-instrumented-refresh')
);
testWindow.electron = electron;
const service = createService();
const operationId = 'playlist-refresh-operation';
await firstValueFrom(
service.updatePlaylist(
'playlist-instrumented-refresh',
{
_id: 'playlist-instrumented-refresh',
playlist: { items: [] },
} as Playlist,
operationId
)
);
expect(electron.dbGetAppPlaylist).toHaveBeenCalledWith(
'playlist-instrumented-refresh',
operationId
);
expect(electron.dbUpsertAppPlaylist).toHaveBeenCalledWith(
expect.objectContaining({
_id: 'playlist-instrumented-refresh',
}),
operationId
);
});
it('keeps both favorites when two rapid favorite adds overlap (SQLite)', async () => {
const { store, electron } = createStatefulElectronStore(
createBasePlaylist('portal-race-two-favorites')
+36 -13
View File
@@ -52,7 +52,10 @@ type PlaylistRawItem = {
type PlaylistStorageElectronApi = {
dbDeleteAllPlaylists: () => Promise<unknown>;
dbDeletePlaylist: (playlistId: string) => Promise<unknown>;
dbGetAppPlaylist: (playlistId: string) => Promise<Playlist | null>;
dbGetAppPlaylist: (
playlistId: string,
operationId?: string
) => Promise<Playlist | null>;
dbGetAppPlaylistFavoriteChannels?: (
playlistId: string
) => Promise<M3uFavoriteChannel[]>;
@@ -60,7 +63,10 @@ type PlaylistStorageElectronApi = {
dbGetAppPlaylists: () => Promise<Playlist[]>;
dbGetAppState: (key: string) => Promise<string | null>;
dbSetAppState: (key: string, value: string) => Promise<unknown>;
dbUpsertAppPlaylist: (playlist: Playlist) => Promise<unknown>;
dbUpsertAppPlaylist: (
playlist: Playlist,
operationId?: string
) => Promise<unknown>;
dbUpsertAppPlaylists: (playlists: Playlist[]) => Promise<unknown>;
};
@@ -362,14 +368,18 @@ export class PlaylistsService {
}
}
private upsertSqlitePlaylist(playlist: Playlist) {
private upsertSqlitePlaylist(playlist: Playlist, operationId?: string) {
return this.runOnSqlite(async () => {
const electron = this.electronApi;
if (!electron) {
return playlist;
}
await electron.dbUpsertAppPlaylist(playlist);
if (operationId === undefined) {
await electron.dbUpsertAppPlaylist(playlist);
} else {
await electron.dbUpsertAppPlaylist(playlist, operationId);
}
return playlist;
});
}
@@ -411,9 +421,14 @@ export class PlaylistsService {
});
}
private persistPlaylistMutation(nextPlaylist: Playlist) {
private persistPlaylistMutation(
nextPlaylist: Playlist,
operationId?: string
) {
if (this.isElectronStorageAvailable) {
return firstValueFrom(this.upsertSqlitePlaylist(nextPlaylist));
return firstValueFrom(
this.upsertSqlitePlaylist(nextPlaylist, operationId)
);
}
return firstValueFrom(
@@ -554,10 +569,14 @@ export class PlaylistsService {
};
}
updatePlaylist(playlistId: string, updatedPlaylist: Playlist) {
updatePlaylist(
playlistId: string,
updatedPlaylist: Playlist,
operationId?: string
) {
return this.serializePlaylistWrite(playlistId, async () => {
const currentPlaylist = await firstValueFrom(
this.getPlaylistById(playlistId)
this.getPlaylistById(playlistId, operationId)
);
const mergedPlaylist = this.mergeRefreshedPlaylist(
currentPlaylist,
@@ -565,17 +584,21 @@ export class PlaylistsService {
playlistId
);
return this.persistPlaylistMutation(mergedPlaylist);
return this.persistPlaylistMutation(mergedPlaylist, operationId);
});
}
getPlaylistById(id: string) {
getPlaylistById(id: string, operationId?: string) {
if (this.isElectronStorageAvailable) {
return this.runOnSqlite(async () => {
const electron = this.electronApi;
const playlist = electron
? await electron.dbGetAppPlaylist(id)
: null;
let playlist: Playlist | null = null;
if (electron) {
playlist =
operationId === undefined
? await electron.dbGetAppPlaylist(id)
: await electron.dbGetAppPlaylist(id, operationId);
}
return playlist
? this.createSqliteFallbackPlaylist(playlist as Playlist)
: (undefined as unknown as Playlist);
+1
View File
@@ -23,6 +23,7 @@ export * from './lib/playback-position.interface';
export * from './lib/playlist-auto-update.interface';
export * from './lib/playlist-backup.interface';
export * from './lib/playlist-meta.type';
export * from './lib/preload-performance-marker.interface';
export * from './lib/playlist-recently-viewed.interface';
export * from './lib/playlist-recently-viewed.utils';
export * from './lib/playlist-refresh.interface';
@@ -684,13 +684,27 @@ export interface ElectronBridgeApi {
dbGetPlaylist: (
playlistId: string
) => Promise<ElectronBridgePlaylistRow | null>;
dbUpsertAppPlaylist: (playlist: Playlist) => Promise<ElectronBridgeResult>;
dbUpsertAppPlaylist: (
playlist: Playlist,
/**
* Instrumentation-only; stripped before the DB invoke.
* Never enters the DB worker payload or persisted playlist data.
*/
operationId?: string
) => Promise<ElectronBridgeResult>;
dbUpsertAppPlaylists: (
playlists: Playlist[]
) => Promise<ElectronBridgeCountResult>;
dbGetAppPlaylists: () => Promise<Playlist[]>;
dbGetAppPlaylistMetas: () => Promise<Playlist[]>;
dbGetAppPlaylist: (playlistId: string) => Promise<Playlist | null>;
dbGetAppPlaylist: (
playlistId: string,
/**
* Instrumentation-only; stripped before the DB invoke.
* Never enters the DB worker payload or persisted playlist data.
*/
operationId?: string
) => Promise<Playlist | null>;
dbGetAppPlaylistFavoriteChannels: (
playlistId: string
) => Promise<M3uFavoriteChannel[]>;
@@ -0,0 +1,52 @@
export const PRELOAD_PERFORMANCE_MARKER_CHANNEL =
'IPTVNATOR_PERF_CAPTURE_MARKER';
export const PRELOAD_PERFORMANCE_METHOD = {
DB_GET_APP_PLAYLIST: 'dbGetAppPlaylist',
DB_UPSERT_APP_PLAYLIST: 'dbUpsertAppPlaylist',
REFRESH_PLAYLIST: 'refreshPlaylist',
} as const;
export type PreloadPerformanceMethod =
(typeof PRELOAD_PERFORMANCE_METHOD)[keyof typeof PRELOAD_PERFORMANCE_METHOD];
export const PRELOAD_PERFORMANCE_PHASE = {
ERROR: 'error',
START: 'start',
SUCCESS: 'success',
} as const;
export type PreloadPerformancePhase =
(typeof PRELOAD_PERFORMANCE_PHASE)[keyof typeof PRELOAD_PERFORMANCE_PHASE];
export const PRELOAD_PERFORMANCE_CORRELATION_STATE = {
COMPLETE: 'complete',
CORRELATED: 'correlated',
INVALID: 'invalid',
UNCORRELATED: 'uncorrelated',
} as const;
export type PreloadPerformanceCorrelationStateName =
(typeof PRELOAD_PERFORMANCE_CORRELATION_STATE)[keyof typeof PRELOAD_PERFORMANCE_CORRELATION_STATE];
export const PRELOAD_PERFORMANCE_INVALID_REASON = {
CONCURRENT_REFRESH: 'concurrent-refresh',
IPC_ERROR: 'ipc-error',
MALFORMED_IDENTIFIER: 'malformed-identifier',
OUT_OF_ORDER: 'out-of-order',
REFRESH_CANCELLED: 'refresh-cancelled',
} as const;
export type PreloadPerformanceInvalidReason =
(typeof PRELOAD_PERFORMANCE_INVALID_REASON)[keyof typeof PRELOAD_PERFORMANCE_INVALID_REASON];
export interface PreloadPerformanceMarker {
method: PreloadPerformanceMethod;
phase: PreloadPerformancePhase;
sourceEpochMs: number;
ipcCallId: number;
playlistId: string | null;
operationId: string | null;
correlationState: PreloadPerformanceCorrelationStateName;
invalidReason: PreloadPerformanceInvalidReason | null;
}