Merge pull request #1280 from 4gray/agent/perf-exact-process-memory

fix(perf): make process memory captures comparison-safe
This commit is contained in:
4gray authored and GitHub committed 2026-07-27 02:57:02 +02:00
commit e1c4853a39
38 files changed
+5828 -222

No files matched your search

+1 -1
View File
@@ -86,7 +86,7 @@ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC phase markers for refresh/DB benchmark correlation; benchmark tooling sets it explicitly, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker timestamps, thread CPU, event-loop utilization, and event-loop delay metrics in database and playlist-refresh responses; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker timestamps, thread CPU, event-loop utilization, and event-loop delay metrics in database and playlist-refresh responses, plus the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
- Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
+5 -4
View File
@@ -142,7 +142,7 @@ Useful narrower flags:
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console logs into the Electron terminal
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC phase markers for refresh/DB benchmark correlation; benchmark tooling sets it explicitly, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker timestamps, thread CPU, event-loop utilization, and event-loop delay metrics in database and playlist-refresh responses; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker timestamps, thread CPU, event-loop utilization, and event-loop delay metrics in database and playlist-refresh responses, plus the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
@@ -357,10 +357,11 @@ Key patterns:
- **Factory injection**: `provideXtreamDataSource()` selects Electron or PWA implementation at runtime
Data strategies by environment:
| Environment | Strategy |
|-------------|----------|
| Environment | Strategy |
| ------------ | ------------------------------------------------------- |
| **Electron** | DB-first: Check DB → fetch API if missing → cache to DB |
| **PWA** | API-only: Always fetch from API, store in memory |
| **PWA** | API-only: Always fetch from API, store in memory |
**M3U Playlist Module Architecture**:
@@ -0,0 +1,122 @@
import type { ElectronApplication, Page } from '@playwright/test';
import {
closeElectronApp,
expect,
launchElectronApp,
test,
type LaunchedElectronApp,
} from './electron-test-fixtures';
import {
installMainCapture,
rolloverMainCapture,
startMainCapture,
stopMainCapture,
} from './performance/m3u-refresh-main-capture';
import type { MainCaptureMetrics } from './performance/m3u-refresh-cancellation-contract';
import type { RendererWindowIdentity } from './performance/renderer-window-rss-session';
test('captures the exact database worker post-GC heap in built Electron', async ({
dataDir,
}) => {
const launchedApps: LaunchedElectronApp[] = [];
try {
const app = await launchElectronApp(dataDir, {
args: ['--js-flags=--expose-gc'],
env: {
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
IPTVNATOR_PERF_CAPTURE: '1',
IPTVNATOR_PERF_WORKER_PROFILING: '1',
},
});
launchedApps.push(app);
await installMainCapture(app.electronApp);
const rendererWindowIdentity = await resolveRendererWindowIdentity(
app.electronApp,
app.mainWindow
);
const captureOptions = {
diagnostic: false,
outputDirectory: dataDir,
rendererWindowIdentity,
} as const;
await startMainCapture(app.electronApp, captureOptions);
await requestDatabaseWorker(app);
const rollover = await rolloverMainCapture(
app.electronApp,
captureOptions
);
expect(rollover.nextCaptureStarted).toBe(true);
expect(rollover.nextCaptureUnavailableReason).toBeNull();
const firstDatabaseWorker = expectExactDatabaseWorker(
rollover.completedCapture
);
await requestDatabaseWorker(app);
const secondDatabaseWorker = expectExactDatabaseWorker(
await stopMainCapture(app.electronApp)
);
expect(secondDatabaseWorker.ordinal).toBe(firstDatabaseWorker.ordinal);
await startMainCapture(app.electronApp, captureOptions);
const noDatabasePhase = await stopMainCapture(app.electronApp);
expect(
noDatabasePhase.workers.filter(
(worker) => worker.kind === 'database.worker'
)
).toHaveLength(0);
} finally {
await Promise.all(launchedApps.map((app) => closeElectronApp(app)));
}
});
async function requestDatabaseWorker(app: LaunchedElectronApp): Promise<void> {
await app.mainWindow.evaluate(() => window.electron.dbGetAppPlaylists());
}
function expectExactDatabaseWorker(capture: MainCaptureMetrics) {
const databaseWorkers = capture.workers.filter(
(worker) => worker.kind === 'database.worker'
);
expect(databaseWorkers).toHaveLength(1);
const databaseWorker = databaseWorkers[0];
expect(databaseWorker?.ordinal).toBeGreaterThan(0);
expect(databaseWorker?.postGcHeapUnavailableReason).toBeNull();
expect(databaseWorker?.postGcHeapUsedBytes).toBeGreaterThan(0);
expect(databaseWorker?.requests).toHaveLength(1);
expect(databaseWorker?.requests[0]).toEqual(
expect.objectContaining({
invalidReason: null,
operation: 'DB_GET_APP_PLAYLISTS',
performanceCaptureUnavailableReason: null,
playlistId: null,
requestId: expect.any(String),
success: true,
})
);
return databaseWorker;
}
async function resolveRendererWindowIdentity(
electronApp: ElectronApplication,
page: Page
): Promise<RendererWindowIdentity> {
const browserWindowHandle = await electronApp.browserWindow(page);
try {
return await browserWindowHandle.evaluate((browserWindow) => {
const exactWindow = browserWindow as unknown as {
readonly id: number;
readonly webContents: { readonly id: number };
};
return {
browserWindowId: exactWindow.id,
webContentsId: exactWindow.webContents.id,
};
});
} finally {
await browserWindowHandle.dispose();
}
}
@@ -0,0 +1,116 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
type RequestDisposition = 'after-cutoff' | 'capture' | 'outside-capture';
interface CutoffApi {
beginCapture(): void;
beginStop(): void;
finishStop(): void;
observeDatabaseRequest(): RequestDisposition;
rolloverCapture?(): void;
snapshot(): {
readonly lateRequestCount: number;
readonly phase: 'active' | 'idle' | 'stopping';
};
}
interface CutoffModule {
createDatabaseWorkerPostGcCutoffApi?: () => CutoffApi;
}
const cutoffModulePromise = import(
new URL('./database-worker-post-gc-cutoff.ts', import.meta.url).href
)
.then((module) => module as CutoffModule)
.catch(() => null);
async function restoreSerializableApi(): Promise<CutoffApi> {
const module = await cutoffModulePromise;
assert.ok(module, 'database worker post-GC cutoff module must exist');
const factory = module.createDatabaseWorkerPostGcCutoffApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => CutoffApi;
return restoredFactory();
}
test('classifies requests after the synchronous capture cutoff without carrying state across generations', async () => {
const api = await restoreSerializableApi();
assert.equal(api.observeDatabaseRequest(), 'outside-capture');
api.beginCapture();
assert.equal(api.observeDatabaseRequest(), 'capture');
api.beginStop();
assert.equal(api.observeDatabaseRequest(), 'after-cutoff');
assert.equal(api.observeDatabaseRequest(), 'after-cutoff');
assert.deepEqual(api.snapshot(), {
lateRequestCount: 2,
phase: 'stopping',
});
api.finishStop();
assert.equal(api.observeDatabaseRequest(), 'outside-capture');
api.beginCapture();
assert.deepEqual(api.snapshot(), {
lateRequestCount: 0,
phase: 'active',
});
});
test('atomically rolls a clean cutoff into the next generation without erasing contamination', async () => {
const clean = await restoreSerializableApi();
assert.equal(typeof clean.rolloverCapture, 'function');
clean.beginCapture();
clean.beginStop();
clean.rolloverCapture?.();
assert.deepEqual(clean.snapshot(), {
lateRequestCount: 0,
phase: 'active',
});
assert.equal(clean.observeDatabaseRequest(), 'capture');
const contaminated = await restoreSerializableApi();
assert.equal(typeof contaminated.rolloverCapture, 'function');
contaminated.beginCapture();
contaminated.beginStop();
assert.equal(contaminated.observeDatabaseRequest(), 'after-cutoff');
assert.throws(
() => contaminated.rolloverCapture?.(),
/database-worker-post-gc-capture-contaminated/
);
assert.deepEqual(contaminated.snapshot(), {
lateRequestCount: 1,
phase: 'stopping',
});
});
test('main capture arms cutoff before awaiting and rejects late DB work before startWorker can restart profiling', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const stopStart = source.indexOf('stop: async (');
const cutoffStart = source.indexOf(
'databaseWorkerPostGcCutoffApi.beginStop()',
stopStart
);
const activeFalse = source.indexOf('state.active = false', stopStart);
const firstAwait = source.indexOf('await ', stopStart);
const observeRequest = source.indexOf(
'databaseWorkerPostGcCutoffApi.observeDatabaseRequest()'
);
const startWorker = source.indexOf('startWorker(record)', observeRequest);
assert.ok(stopStart >= 0);
assert.ok(cutoffStart > stopStart && cutoffStart < firstAwait);
assert.ok(activeFalse > stopStart && activeFalse < firstAwait);
assert.ok(observeRequest >= 0 && observeRequest < startWorker);
assert.match(source, /database-worker-activity-after-cutoff/);
});
@@ -0,0 +1,74 @@
export type DatabaseWorkerPostGcRequestDisposition =
'after-cutoff' | 'capture' | 'outside-capture';
export interface DatabaseWorkerPostGcCutoffSnapshot {
readonly lateRequestCount: number;
readonly phase: 'active' | 'idle' | 'stopping';
}
export interface DatabaseWorkerPostGcCutoffApi {
beginCapture(): void;
beginStop(): void;
finishStop(): void;
observeDatabaseRequest(): DatabaseWorkerPostGcRequestDisposition;
rolloverCapture(): void;
snapshot(): DatabaseWorkerPostGcCutoffSnapshot;
}
export function createDatabaseWorkerPostGcCutoffApi(): DatabaseWorkerPostGcCutoffApi {
let lateRequestCount = 0;
let phase: DatabaseWorkerPostGcCutoffSnapshot['phase'] = 'idle';
const api: DatabaseWorkerPostGcCutoffApi = {
beginCapture(): void {
if (phase !== 'idle') {
throw new Error(
phase === 'stopping'
? 'database-worker-post-gc-stop-in-progress'
: 'database-worker-post-gc-capture-already-active'
);
}
lateRequestCount = 0;
phase = 'active';
},
beginStop(): void {
if (phase !== 'active') {
throw new Error('database-worker-post-gc-capture-not-active');
}
phase = 'stopping';
},
finishStop(): void {
phase = 'idle';
},
observeDatabaseRequest(): DatabaseWorkerPostGcRequestDisposition {
if (phase === 'active') {
return 'capture';
}
if (phase === 'stopping') {
lateRequestCount += 1;
return 'after-cutoff';
}
return 'outside-capture';
},
rolloverCapture(): void {
if (phase !== 'stopping') {
throw new Error('database-worker-post-gc-stop-not-in-progress');
}
if (lateRequestCount > 0) {
throw new Error('database-worker-post-gc-capture-contaminated');
}
lateRequestCount = 0;
phase = 'active';
},
snapshot(): DatabaseWorkerPostGcCutoffSnapshot {
return Object.freeze({ lateRequestCount, phase });
},
};
return Object.freeze(api);
}
@@ -0,0 +1,392 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
type AncillaryFailureStage = 'heap-snapshot' | 'post-gc-probe' | 'profile-stop';
type PostGcOutcome =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason: string;
};
interface FinalizationInput {
readonly finalizationKey: object;
readonly joinFinalSample: () => Promise<void>;
readonly probePostGc: () => Promise<PostGcOutcome>;
readonly reportAncillaryFailure: (
stage: AncillaryFailureStage,
error: unknown
) => void;
readonly stopProfile: () => Promise<void>;
readonly stopSampling: () => void;
readonly takeHeapSnapshot?: () => Promise<void>;
}
interface FinalizationApi {
finalize(input: FinalizationInput): Promise<PostGcOutcome>;
}
interface FinalizationModule {
createDatabaseWorkerPostGcFinalizationApi?: () => FinalizationApi;
}
const finalizationModulePromise = import(
new URL('./database-worker-post-gc-finalization.ts', import.meta.url).href
)
.then((module) => module as FinalizationModule)
.catch(() => null);
function deferred(): {
readonly promise: Promise<void>;
readonly resolve: () => void;
} {
let resolve!: () => void;
const promise = new Promise<void>((resolvePromise) => {
resolve = resolvePromise;
});
return { promise, resolve };
}
async function restoreSerializableApi(): Promise<FinalizationApi> {
const module = await finalizationModulePromise;
assert.ok(module, 'database worker post-GC finalization module must exist');
const factory = module.createDatabaseWorkerPostGcFinalizationApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => FinalizationApi;
return restoredFactory();
}
test('coordinates database worker post-GC finalization in order and single-flight', async () => {
const api = await restoreSerializableApi();
const finalSampleGate = deferred();
const events: string[] = [];
let probeCalls = 0;
const successfulPostGc = {
postGcHeapUsedBytes: 98_304,
unavailableReason: null,
} as const;
const input: FinalizationInput = {
finalizationKey: {},
joinFinalSample: async () => {
events.push('final-sample:start');
await finalSampleGate.promise;
events.push('final-sample:joined');
},
probePostGc: async () => {
probeCalls += 1;
events.push('post-gc-probe');
return successfulPostGc;
},
reportAncillaryFailure: () => {
assert.fail('the successful path has no ancillary failures');
},
stopProfile: async () => {
events.push('profile-stop');
},
stopSampling: () => {
events.push('sampling-stop');
},
takeHeapSnapshot: async () => {
events.push('heap-snapshot');
},
};
const firstFinalization = api.finalize(input);
const concurrentFinalization = api.finalize(input);
await Promise.resolve();
assert.deepEqual(events, ['sampling-stop', 'final-sample:start']);
assert.equal(probeCalls, 0);
finalSampleGate.resolve();
const [firstResult, concurrentResult] = await Promise.all([
firstFinalization,
concurrentFinalization,
]);
assert.deepEqual(events, [
'sampling-stop',
'final-sample:start',
'final-sample:joined',
'profile-stop',
'post-gc-probe',
'heap-snapshot',
]);
assert.equal(probeCalls, 1);
assert.deepEqual(firstResult, successfulPostGc);
assert.deepEqual(concurrentResult, successfulPostGc);
});
test('reports profile and snapshot failures without erasing successful post-GC', async () => {
const api = await restoreSerializableApi();
const events: string[] = [];
const profileError = new Error('profile-stop-failed');
const snapshotError = new Error('heap-snapshot-failed');
const failures: {
readonly error: unknown;
readonly stage: AncillaryFailureStage;
}[] = [];
const result = await api.finalize({
finalizationKey: {},
joinFinalSample: async () => {
events.push('final-sample:joined');
},
probePostGc: async () => {
events.push('post-gc-probe');
return {
postGcHeapUsedBytes: 131_072,
unavailableReason: null,
};
},
reportAncillaryFailure: (stage, error) => {
failures.push({ error, stage });
},
stopProfile: async () => {
events.push('profile-stop');
throw profileError;
},
stopSampling: () => {
events.push('sampling-stop');
},
takeHeapSnapshot: async () => {
events.push('heap-snapshot');
throw snapshotError;
},
});
assert.deepEqual(events, [
'sampling-stop',
'final-sample:joined',
'profile-stop',
'post-gc-probe',
'heap-snapshot',
]);
assert.deepEqual(failures, [
{ error: profileError, stage: 'profile-stop' },
{ error: snapshotError, stage: 'heap-snapshot' },
]);
assert.deepEqual(result, {
postGcHeapUsedBytes: 131_072,
unavailableReason: null,
});
});
test('fails closed when the post-GC probe throws without relabelling profile artifacts', async () => {
const api = await restoreSerializableApi();
const probeError = new Error('probe-failed');
const failures: {
readonly error: unknown;
readonly stage: AncillaryFailureStage;
}[] = [];
const result = await api.finalize({
finalizationKey: {},
joinFinalSample: async () => undefined,
probePostGc: async () => {
throw probeError;
},
reportAncillaryFailure: (stage, error) => {
failures.push({ error, stage });
},
stopProfile: async () => undefined,
stopSampling: () => undefined,
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'capture-failed',
});
assert.deepEqual(failures, [{ error: probeError, stage: 'post-gc-probe' }]);
});
test('the Electron main capture wires exact DB selection and explicit-GC finalization', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
assert.match(
source,
/databaseWorkerPostGcSelectionApiFactorySource:\s+createDatabaseWorkerPostGcSelectionApi\.toString\(\)/
);
assert.match(
source,
/databaseWorkerPostGcProbeApiFactorySource:\s+createDatabaseWorkerPostGcProbeApi\.toString\(\)/
);
assert.match(
source,
/databaseWorkerPostGcFinalizationApiFactorySource:\s+createDatabaseWorkerPostGcFinalizationApi\.toString\(\)/
);
assert.match(source, /new workerThreads\.MessageChannel\(\)/);
assert.match(
source,
/databaseWorkerPostGcSelectionApi\.select\(\s*currentWorkerRecords,\s*state\.captureGeneration\s*\)/
);
assert.match(source, /databaseWorkerPostGcFinalizationApi\s*\.finalize\(/);
assert.match(source, /pendingCount: 0/);
assert.match(source, /record\.pendingCount \+= 1/);
assert.match(
source,
/request\.record\.pendingCount = Math\.max\(\s*0,\s*request\.record\.pendingCount - 1\s*\)/
);
assert.match(source, /samplePromise/);
assert.match(source, /postGcHeapUnavailableReason/);
assert.match(source, /ordinal: nextWorkerOrdinal/);
assert.match(
source,
/`\$\{record\.kind\}-\$\{record\.ordinal\}\.cpuprofile`/
);
assert.doesNotMatch(source, /sampleBusy/);
assert.doesNotMatch(source, /const postSnapshot/);
});
test('main CPU profiling stops at the operation cutoff before worker artifact finalization', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const stopStart = source.indexOf('stop: async (');
const mainProfileStop = source.indexOf("'Profiler.stop'", stopStart);
const databaseSelection = source.indexOf(
'databaseWorkerPostGcSelectionApi.select',
stopStart
);
const databaseFinalization = source.indexOf(
'await finalizeDatabaseWorker',
stopStart
);
assert.ok(stopStart >= 0);
assert.ok(mainProfileStop > stopStart);
assert.ok(mainProfileStop < databaseSelection);
assert.ok(mainProfileStop < databaseFinalization);
});
test('worker CPU profile serialization runs only after the main CPU profiler stops', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const profileStopStart = source.indexOf('const stopWorkerProfile');
const profileStopEnd = source.indexOf(
'const writeWorkerProfile',
profileStopStart
);
const profileStop = source.slice(profileStopStart, profileStopEnd);
const stopStart = source.indexOf('stop: async (');
const mainProfileStop = source.indexOf("'Profiler.stop'", stopStart);
const workerProfileFlush = source.indexOf(
'flushWorkerProfiles(currentWorkerRecords)',
mainProfileStop
);
assert.match(profileStop, /const profileResult = await handle\.stop\(\)/);
assert.match(profileStop, /record\.profileResult = profileResult/);
assert.doesNotMatch(profileStop, /writeFileSync|JSON\.stringify/);
assert.ok(mainProfileStop > stopStart);
assert.ok(workerProfileFlush > mainProfileStop);
});
test('measured termination stays unperturbed while diagnostic capture finalizes its profile before termination', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const terminateStart = source.indexOf(
'WorkerClass.prototype.terminate = function'
);
const terminateEnd = source.indexOf('const inspectorPost', terminateStart);
const terminateBlock = source.slice(terminateStart, terminateEnd);
const terminatingFinalizerStart = source.indexOf(
'const finalizeTerminatingWorker'
);
const terminatingFinalizerEnd = source.indexOf(
'WorkerClass.prototype.postMessage',
terminatingFinalizerStart
);
const terminatingFinalizer = source.slice(
terminatingFinalizerStart,
terminatingFinalizerEnd
);
const measuredTerminate = terminateBlock.indexOf(
'const termination = originalTerminate.call(this)'
);
const measuredFinalization = terminateBlock.indexOf(
'void finalizeTerminatingWorker(record, false)'
);
const diagnosticFinalizationStart = terminateBlock.indexOf(
'void finalizeTerminatingWorker(record, true)'
);
const diagnosticFinalizationWait = terminateBlock.indexOf(
'await waitForTerminatingWorkerFinalization(record)'
);
const diagnosticTerminate = terminateBlock.indexOf(
'return originalTerminate.call(this)',
diagnosticFinalizationWait
);
assert.ok(
diagnosticFinalizationStart >= 0 &&
diagnosticFinalizationStart < diagnosticFinalizationWait &&
diagnosticFinalizationWait < diagnosticTerminate,
'diagnostic capture must start profile finalization, bound its wait, and then terminate the worker'
);
assert.ok(
measuredTerminate >= 0 && measuredTerminate < measuredFinalization,
'measured capture must initiate termination before best-effort finalization'
);
assert.doesNotMatch(terminatingFinalizer, /takeWorkerHeapSnapshot/);
assert.match(
terminatingFinalizer,
/stopWorkerProfile\(record, waitForProfileHandle\)/
);
assert.match(source, /resolvedProfileHandle/);
assert.match(source, /worker-profile-finalization-timeout/);
assert.match(source, /finalizationTimedOut/);
assert.match(source, /profileCaptureKey/);
assert.match(source, /record\.profileCaptureKey !== profileCaptureKey/);
});
test('worker capture failures use an artifact-neutral timeline label', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
assert.match(source, /worker-artifact-error:\$\{stage\}/);
assert.doesNotMatch(source, /worker-profile-error:\$\{stage\}/);
});
test('capture generation resets artifact paths and always emits current DB records', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const start = source.indexOf('const startCapture = async (');
const diagnostic = source.indexOf('if (state.diagnostic)', start);
const profileReset = source.indexOf('state.mainProfilePath = null', start);
const snapshotReset = source.indexOf(
'state.mainSnapshotPath = null',
start
);
assert.ok(profileReset > start && profileReset < diagnostic);
assert.ok(snapshotReset > start && snapshotReset < diagnostic);
assert.match(
source,
/record\.captureGeneration === state\.captureGeneration[\s\S]*record\.kind === 'database\.worker'/
);
});
@@ -0,0 +1,90 @@
import type { DatabaseWorkerPostGcProbeUnavailableReason } from './database-worker-post-gc-probe';
import type { DatabaseWorkerPostGcUnavailableReason } from './database-worker-post-gc-selection';
export type DatabaseWorkerPostGcAncillaryFailureStage =
'heap-snapshot' | 'post-gc-probe' | 'profile-stop' | 'profile-write';
export type DatabaseWorkerPostGcFinalizationOutcome =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason:
| DatabaseWorkerPostGcProbeUnavailableReason
| DatabaseWorkerPostGcUnavailableReason;
};
export interface DatabaseWorkerPostGcFinalizationInput {
readonly finalizationKey: object;
readonly joinFinalSample: () => Promise<void>;
readonly probePostGc: () => Promise<DatabaseWorkerPostGcFinalizationOutcome>;
readonly reportAncillaryFailure: (
stage: DatabaseWorkerPostGcAncillaryFailureStage,
error: unknown
) => void;
readonly stopProfile: () => Promise<void>;
readonly stopSampling: () => void;
readonly takeHeapSnapshot?: () => Promise<void>;
}
export interface DatabaseWorkerPostGcFinalizationApi {
finalize(
input: DatabaseWorkerPostGcFinalizationInput
): Promise<DatabaseWorkerPostGcFinalizationOutcome>;
}
export function createDatabaseWorkerPostGcFinalizationApi(): DatabaseWorkerPostGcFinalizationApi {
const finalizations = new WeakMap<
object,
Promise<DatabaseWorkerPostGcFinalizationOutcome>
>();
const helpers = {
async run(
input: DatabaseWorkerPostGcFinalizationInput
): Promise<DatabaseWorkerPostGcFinalizationOutcome> {
input.stopSampling();
await input.joinFinalSample();
try {
await input.stopProfile();
} catch (error: unknown) {
input.reportAncillaryFailure('profile-stop', error);
}
let outcome: DatabaseWorkerPostGcFinalizationOutcome;
try {
outcome = await input.probePostGc();
} catch (error: unknown) {
input.reportAncillaryFailure('post-gc-probe', error);
outcome = {
postGcHeapUsedBytes: null,
unavailableReason: 'capture-failed',
};
}
if (input.takeHeapSnapshot) {
try {
await input.takeHeapSnapshot();
} catch (error: unknown) {
input.reportAncillaryFailure('heap-snapshot', error);
}
}
return outcome;
},
finalize(
input: DatabaseWorkerPostGcFinalizationInput
): Promise<DatabaseWorkerPostGcFinalizationOutcome> {
const existing = finalizations.get(input.finalizationKey);
if (existing) {
return existing;
}
const finalization = helpers.run(input);
finalizations.set(input.finalizationKey, finalization);
return finalization;
},
};
return Object.freeze({ finalize: helpers.finalize });
}
@@ -0,0 +1,469 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
/* eslint-disable max-lines -- The one-shot transport contract keeps all terminal-path fixtures together. */
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
type WorkerUnavailableReason =
'capture-failed' | 'gc-unavailable' | 'profiling-disabled' | 'worker-busy';
type MainUnavailableReason =
| 'post-gc-probe-invalid-response'
| 'post-gc-probe-message-error'
| 'post-gc-probe-port-closed'
| 'post-gc-probe-post-failed'
| 'post-gc-probe-timeout';
type ProbeUnavailableReason = WorkerUnavailableReason | MainUnavailableReason;
type ProbeResult =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason: ProbeUnavailableReason;
};
interface ProbePort {
close(): void;
off(event: 'close', listener: () => void): unknown;
off(event: 'message', listener: (message: unknown) => void): unknown;
off(event: 'messageerror', listener: (error: unknown) => void): unknown;
on(event: 'close', listener: () => void): unknown;
on(event: 'message', listener: (message: unknown) => void): unknown;
on(event: 'messageerror', listener: (error: unknown) => void): unknown;
postMessage(message: unknown): void;
start(): void;
}
interface ProbeWorker {
postMessage(message: unknown, transferList: readonly unknown[]): void;
}
interface ProbeTimers {
clearTimeout(handle: unknown): void;
setTimeout(callback: () => void, delayMs: number): unknown;
}
interface ProbeApi {
probe(input: {
readonly createMessageChannel: () => {
readonly port1: ProbePort;
readonly port2: ProbePort;
};
readonly timeoutMs?: number;
readonly timers?: ProbeTimers;
readonly worker: ProbeWorker;
}): Promise<ProbeResult>;
}
interface ProbeModule {
createDatabaseWorkerPostGcProbeApi?: () => ProbeApi;
}
interface ProductionWorkerPostGcModule {
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON?: Readonly<
Record<string, WorkerUnavailableReason>
>;
}
const probeModulePromise = import(
new URL('./database-worker-post-gc-probe.ts', import.meta.url).href
)
.then((module) => module as ProbeModule)
.catch(() => null);
const productionWorkerPostGcModulePromise = import(
new URL(
'../../../electron-backend/src/app/workers/database-worker-post-gc-heap.ts',
import.meta.url
).href
)
.then((module) => module as ProductionWorkerPostGcModule)
.catch(() => null);
class FakePort extends EventEmitter implements ProbePort {
closeCalls = 0;
peer: FakePort | null = null;
startCalls = 0;
close(): void {
this.closeCalls += 1;
}
postMessage(message: unknown): void {
this.peer?.emit('message', message);
}
start(): void {
this.startCalls += 1;
}
}
class FakeTimers implements ProbeTimers {
readonly cleared: unknown[] = [];
readonly scheduled: {
readonly callback: () => void;
readonly delayMs: number;
readonly handle: number;
}[] = [];
private nextHandle = 1;
clearTimeout(handle: unknown): void {
this.cleared.push(handle);
}
fire(handle: number): void {
const timer = this.scheduled.find(
(candidate) => candidate.handle === handle
);
assert.ok(timer, `timer ${handle} must exist`);
timer.callback();
}
setTimeout(callback: () => void, delayMs: number): number {
const handle = this.nextHandle;
this.nextHandle += 1;
this.scheduled.push({ callback, delayMs, handle });
return handle;
}
}
interface ProbeHarness {
readonly createMessageChannel: () => {
readonly port1: FakePort;
readonly port2: FakePort;
};
readonly port1: FakePort;
readonly port2: FakePort;
readonly timers: FakeTimers;
}
function createProbeHarness(): ProbeHarness {
const port1 = new FakePort();
const port2 = new FakePort();
port1.peer = port2;
port2.peer = port1;
const timers = new FakeTimers();
return {
createMessageChannel: () => ({ port1, port2 }),
port1,
port2,
timers,
};
}
async function restoreSerializableApi(): Promise<ProbeApi> {
const module = await probeModulePromise;
assert.ok(module, 'database worker post-GC probe module must exist');
const factory = module.createDatabaseWorkerPostGcProbeApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => ProbeApi;
return restoredFactory();
}
function assertCoherentResult(result: ProbeResult): void {
const hasHeap =
Number.isSafeInteger(result.postGcHeapUsedBytes) &&
Number(result.postGcHeapUsedBytes) >= 0;
assert.equal(
hasHeap,
result.unavailableReason === null,
'probe result must preserve the heap/reason XOR'
);
}
function cleanupCounts(port: FakePort): Record<string, number> {
return {
close: port.listenerCount('close'),
message: port.listenerCount('message'),
messageerror: port.listenerCount('messageerror'),
};
}
test('serializes the factory and sends the exact one-shot worker request with its transfer port', async () => {
const api = await restoreSerializableApi();
const harness = createProbeHarness();
let postedMessage: unknown;
let postedTransferList: readonly unknown[] | null = null;
let terminateCalls = 0;
const worker = {
postMessage(message: unknown, transferList: readonly unknown[]): void {
postedMessage = message;
postedTransferList = transferList;
const responsePort = (
message as { readonly responsePort: ProbePort }
).responsePort;
responsePort.postMessage({
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: 73_728,
unavailableReason: null,
});
},
terminate(): void {
terminateCalls += 1;
},
};
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker,
});
assert.deepEqual(postedMessage, {
type: 'performance:collect-post-gc-heap',
responsePort: harness.port2,
});
assert.deepEqual(postedTransferList, [harness.port2]);
assert.deepEqual(result, {
postGcHeapUsedBytes: 73_728,
unavailableReason: null,
});
assertCoherentResult(result);
assert.equal(harness.port1.startCalls, 1);
assert.equal(harness.port1.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
assert.deepEqual(
harness.timers.scheduled.map((timer) => timer.delayMs),
[5_000]
);
assert.deepEqual(harness.timers.cleared, [1]);
assert.equal(terminateCalls, 0);
});
test('preserves every coherent worker-side unavailable result', async () => {
const api = await restoreSerializableApi();
const productionModule = await productionWorkerPostGcModulePromise;
assert.ok(productionModule, 'production worker post-GC module must load');
const productionReasons =
productionModule.DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON;
assert.ok(productionReasons, 'production worker reasons must be exported');
const reasons = Object.values(productionReasons);
assert.deepEqual(
new Set(reasons),
new Set<WorkerUnavailableReason>([
'capture-failed',
'gc-unavailable',
'profiling-disabled',
'worker-busy',
])
);
for (const unavailableReason of reasons) {
const harness = createProbeHarness();
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(message: unknown): void {
(
message as { readonly responsePort: ProbePort }
).responsePort.postMessage({
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason,
});
},
},
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason,
});
assertCoherentResult(result);
}
});
test('fails closed for malformed or incoherent worker responses', async () => {
const api = await restoreSerializableApi();
const malformedResponses: readonly unknown[] = [
null,
[],
{
type: 'wrong-result',
postGcHeapUsedBytes: 1,
unavailableReason: null,
},
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: 1,
unavailableReason: 'capture-failed',
},
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason: null,
},
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason: 'unknown-reason',
},
...[-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY].map(
(postGcHeapUsedBytes) => ({
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes,
unavailableReason: null,
})
),
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: Number.MAX_SAFE_INTEGER + 1,
unavailableReason: null,
},
];
for (const response of malformedResponses) {
const harness = createProbeHarness();
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(message: unknown): void {
(
message as { readonly responsePort: ProbePort }
).responsePort.postMessage(response);
},
},
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'post-gc-probe-invalid-response',
});
assertCoherentResult(result);
}
});
test('times out once with an injectable deadline and ignores every later terminal signal', async () => {
const api = await restoreSerializableApi();
const harness = createProbeHarness();
let terminateCalls = 0;
const resultPromise = api.probe({
createMessageChannel: harness.createMessageChannel,
timeoutMs: 37,
timers: harness.timers,
worker: {
postMessage(): void {
// Keep the one-shot probe pending until its bounded deadline.
},
terminate(): void {
terminateCalls += 1;
},
},
});
const timer = harness.timers.scheduled[0];
assert.ok(timer);
assert.equal(timer.delayMs, 37);
harness.timers.fire(timer.handle);
const result = await resultPromise;
harness.port1.emit('message', {
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: 99,
unavailableReason: null,
});
harness.port1.emit('messageerror', new Error('late message error'));
harness.port1.emit('close');
timer.callback();
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'post-gc-probe-timeout',
});
assertCoherentResult(result);
assert.equal(harness.port1.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
assert.deepEqual(harness.timers.cleared, [timer.handle]);
assert.equal(terminateCalls, 0);
});
test('maps message errors and an early response-port close to fixed reasons', async () => {
const api = await restoreSerializableApi();
const cases = [
{
emit(port: FakePort): void {
port.emit('messageerror', new Error('clone failed'));
},
reason: 'post-gc-probe-message-error',
},
{
emit(port: FakePort): void {
port.emit('close');
},
reason: 'post-gc-probe-port-closed',
},
] as const;
for (const testCase of cases) {
const harness = createProbeHarness();
const resultPromise = api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(): void {
testCase.emit(harness.port1);
},
},
});
const result = await resultPromise;
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: testCase.reason,
});
assertCoherentResult(result);
assert.equal(harness.port1.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
}
});
test('closes both ports and reports a fixed reason when the request cannot be posted', async () => {
const api = await restoreSerializableApi();
const harness = createProbeHarness();
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(): void {
throw new Error('worker already exited');
},
},
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'post-gc-probe-post-failed',
});
assertCoherentResult(result);
assert.equal(harness.port1.closeCalls, 1);
assert.equal(harness.port2.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
assert.deepEqual(harness.timers.cleared, [1]);
});
@@ -0,0 +1,242 @@
export type DatabaseWorkerPostGcProbeUnavailableReason =
| 'capture-failed'
| 'gc-unavailable'
| 'post-gc-probe-invalid-response'
| 'post-gc-probe-message-error'
| 'post-gc-probe-port-closed'
| 'post-gc-probe-post-failed'
| 'post-gc-probe-timeout'
| 'profiling-disabled'
| 'worker-busy';
export type DatabaseWorkerPostGcProbeResult =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason: DatabaseWorkerPostGcProbeUnavailableReason;
};
export interface DatabaseWorkerPostGcProbePort {
close(): void;
off(event: 'close', listener: () => void): unknown;
off(event: 'message', listener: (message: unknown) => void): unknown;
off(event: 'messageerror', listener: (error: unknown) => void): unknown;
on(event: 'close', listener: () => void): unknown;
on(event: 'message', listener: (message: unknown) => void): unknown;
on(event: 'messageerror', listener: (error: unknown) => void): unknown;
start(): void;
}
export interface DatabaseWorkerPostGcProbeWorker {
postMessage(message: unknown, transferList: readonly unknown[]): void;
}
export interface DatabaseWorkerPostGcProbeTimers {
clearTimeout(handle: unknown): void;
setTimeout(callback: () => void, delayMs: number): unknown;
}
export interface DatabaseWorkerPostGcProbeInput {
readonly createMessageChannel: () => {
readonly port1: DatabaseWorkerPostGcProbePort;
readonly port2: DatabaseWorkerPostGcProbePort;
};
readonly timeoutMs?: number;
readonly timers?: DatabaseWorkerPostGcProbeTimers;
readonly worker: DatabaseWorkerPostGcProbeWorker;
}
export interface DatabaseWorkerPostGcProbeApi {
probe(
input: DatabaseWorkerPostGcProbeInput
): Promise<DatabaseWorkerPostGcProbeResult>;
}
export function createDatabaseWorkerPostGcProbeApi(): DatabaseWorkerPostGcProbeApi {
type JsonRecord = Record<string, unknown>;
const DEFAULT_TIMEOUT_MS = 5_000;
const REQUEST_TYPE = 'performance:collect-post-gc-heap';
const RESULT_TYPE = 'performance:post-gc-heap-result';
const workerUnavailableReasons = new Set([
'capture-failed',
'gc-unavailable',
'profiling-disabled',
'worker-busy',
]);
const defaultTimers: DatabaseWorkerPostGcProbeTimers = {
clearTimeout(handle: unknown): void {
globalThis.clearTimeout(
handle as ReturnType<typeof globalThis.setTimeout>
);
},
setTimeout(callback: () => void, delayMs: number): unknown {
return globalThis.setTimeout(callback, delayMs);
},
};
const helpers = {
unavailable(
unavailableReason: DatabaseWorkerPostGcProbeUnavailableReason
): DatabaseWorkerPostGcProbeResult {
return Object.freeze({
postGcHeapUsedBytes: null,
unavailableReason,
});
},
normalizeResponse(response: unknown): DatabaseWorkerPostGcProbeResult {
if (
typeof response !== 'object' ||
response === null ||
Array.isArray(response)
) {
return helpers.unavailable('post-gc-probe-invalid-response');
}
const candidate = response as JsonRecord;
const postGcHeapUsedBytes = candidate['postGcHeapUsedBytes'];
const unavailableReason = candidate['unavailableReason'];
if (
candidate['type'] === RESULT_TYPE &&
Number.isSafeInteger(postGcHeapUsedBytes) &&
Number(postGcHeapUsedBytes) >= 0 &&
unavailableReason === null
) {
return Object.freeze({
postGcHeapUsedBytes: Number(postGcHeapUsedBytes),
unavailableReason: null,
});
}
if (
candidate['type'] === RESULT_TYPE &&
postGcHeapUsedBytes === null &&
typeof unavailableReason === 'string' &&
workerUnavailableReasons.has(unavailableReason)
) {
return helpers.unavailable(
unavailableReason as DatabaseWorkerPostGcProbeUnavailableReason
);
}
return helpers.unavailable('post-gc-probe-invalid-response');
},
probe(
input: DatabaseWorkerPostGcProbeInput
): Promise<DatabaseWorkerPostGcProbeResult> {
let channel: ReturnType<typeof input.createMessageChannel>;
try {
channel = input.createMessageChannel();
} catch {
return Promise.resolve(
helpers.unavailable('post-gc-probe-post-failed')
);
}
const { port1, port2 } = channel;
const timers = input.timers ?? defaultTimers;
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
return new Promise((resolve) => {
let settled = false;
let timerHandle: unknown;
let timerScheduled = false;
const callbacks = {
cleanup(): void {
try {
port1.off('message', callbacks.onMessage);
} catch {
// Best-effort profiling cleanup must not escape.
}
try {
port1.off('messageerror', callbacks.onMessageError);
} catch {
// Best-effort profiling cleanup must not escape.
}
try {
port1.off('close', callbacks.onClose);
} catch {
// Best-effort profiling cleanup must not escape.
}
if (timerScheduled) {
try {
timers.clearTimeout(timerHandle);
} catch {
// Best-effort profiling cleanup must not escape.
}
}
try {
port1.close();
} catch {
// The one-shot response port may already be closed.
}
},
settle(result: DatabaseWorkerPostGcProbeResult): void {
if (settled) {
return;
}
settled = true;
callbacks.cleanup();
resolve(result);
},
onClose(): void {
callbacks.settle(
helpers.unavailable('post-gc-probe-port-closed')
);
},
onMessage(message: unknown): void {
callbacks.settle(helpers.normalizeResponse(message));
},
onMessageError(): void {
callbacks.settle(
helpers.unavailable('post-gc-probe-message-error')
);
},
onTimeout(): void {
callbacks.settle(
helpers.unavailable('post-gc-probe-timeout')
);
},
};
try {
port1.on('message', callbacks.onMessage);
port1.on('messageerror', callbacks.onMessageError);
port1.on('close', callbacks.onClose);
port1.start();
timerHandle = timers.setTimeout(
callbacks.onTimeout,
timeoutMs
);
timerScheduled = true;
input.worker.postMessage(
{
type: REQUEST_TYPE,
responsePort: port2,
},
[port2]
);
} catch {
callbacks.settle(
helpers.unavailable('post-gc-probe-post-failed')
);
try {
port2.close();
} catch {
// A synchronous transfer failure may already close it.
}
}
});
},
};
return Object.freeze({ probe: helpers.probe });
}
@@ -0,0 +1,131 @@
/* 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 CancellationBenchmarkManifest,
type CancellationIterationResult,
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
type WorkerCaptureMetrics,
} from './m3u-refresh-cancellation-contract';
import { createCancellationBenchmarkSummary } from './m3u-refresh-cancellation-report';
function databaseWorker(
postGcHeapUsedBytes: number | null,
postGcHeapUnavailableReason: string | null
): WorkerCaptureMetrics {
return {
kind: PERFORMANCE_WORKER_KIND.DATABASE,
peakExternalBytes: 0,
peakHeapUsedBytes: 0,
postGcHeapUnavailableReason,
postGcHeapUsedBytes,
requests: [],
} as unknown as WorkerCaptureMetrics;
}
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,
},
rendererWindow: {
responsiveEvents: 0,
rss: {
identity: {
creationTime: 1_721_234_567_890,
pid: 42,
},
missingSampleCount: 0,
peakRssBytes: 2_048,
unavailableReason: null,
validSampleCount: 1,
},
unresponsiveEvents: 0,
windowIdentity: {
browserWindowId: 7,
webContentsId: 11,
},
},
workers,
},
phases: {},
renderer: {
peakHeapUsedBytes: 0,
postGcHeapUsedBytes: null,
probe: {
frameGapsMs: [],
heartbeatDelaysMs: [],
longTasksMs: [],
},
},
runId: 'measured',
} as unknown as CancellationIterationResult;
}
test('summary includes every database isolate instead of silently taking the first', () => {
const unavailable = databaseWorker(null, 'post-gc-probe-invalid-response');
const summary = createCancellationBenchmarkSummary(
{} as CancellationBenchmarkManifest,
[
measuredIteration([
databaseWorker(100, null),
databaseWorker(300, null),
unavailable,
]),
]
);
assert.deepEqual(summary.measured.databaseWorkerPostGcHeapBytes, {
count: 2,
max: 300,
mean: 200,
median: 200,
min: 100,
p95: 290,
p99: 298,
});
assert.deepEqual(
(
summary as unknown as {
readonly validity: {
readonly databaseWorkerPostGc: unknown;
};
}
).validity.databaseWorkerPostGc,
{
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 3,
reason: 'database-worker-unexpected-activity',
runId: 'measured',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
}
);
assert.equal(
(
summary.iterations[0]?.main.workers[2] as WorkerCaptureMetrics & {
readonly postGcHeapUnavailableReason: string | null;
}
)?.postGcHeapUnavailableReason,
'post-gc-probe-invalid-response'
);
});
@@ -0,0 +1,145 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import test from 'node:test';
type WorkerKind = 'database.worker' | 'playlist-refresh.worker';
interface TestWorkerRecord {
readonly captureGeneration: number | null;
readonly finalized: boolean;
readonly kind: WorkerKind;
readonly ordinal: number;
readonly pendingCount: number;
readonly sampleTimer: object | null;
}
type SelectionReason =
| 'database-worker-missing'
| 'database-worker-not-idle'
| 'multiple-database-workers';
interface Selection<T> {
readonly selected: T | null;
readonly unavailableReason: SelectionReason | null;
}
type Selector = <T extends TestWorkerRecord>(
records: readonly T[],
activeGeneration: number
) => Selection<T>;
interface SelectionModule {
createDatabaseWorkerPostGcSelectionApi?: () => {
readonly select: Selector;
};
}
const selectionModulePromise = import(
new URL('./database-worker-post-gc-selection.ts', import.meta.url).href
)
.then((module) => module as SelectionModule)
.catch(() => null);
function workerRecord(
ordinal: number,
overrides: Partial<TestWorkerRecord> = {}
): TestWorkerRecord {
return {
captureGeneration: 7,
finalized: true,
kind: 'database.worker',
ordinal,
pendingCount: 0,
sampleTimer: null,
...overrides,
};
}
async function restoreSerializableSelector(): Promise<Selector> {
const module = await selectionModulePromise;
assert.ok(module, 'database worker post-GC selector module must exist');
const factory = module.createDatabaseWorkerPostGcSelectionApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => {
readonly select: Selector;
};
return restoredFactory().select;
}
test('selects the only current-generation database worker independently of sampling and finalization state', async () => {
const select = await restoreSerializableSelector();
const currentDatabase = workerRecord(4, {
finalized: true,
sampleTimer: null,
});
const previousDatabase = workerRecord(1, {
captureGeneration: 6,
finalized: false,
sampleTimer: {},
});
const currentPlaylist = workerRecord(2, {
finalized: false,
kind: 'playlist-refresh.worker',
sampleTimer: {},
});
const selection = select(
[previousDatabase, currentPlaylist, currentDatabase],
7
);
assert.equal(selection.selected, currentDatabase);
assert.deepEqual(selection, {
selected: currentDatabase,
unavailableReason: null,
});
});
test('reports a missing database worker after ignoring playlists and previous generations', async () => {
const select = await restoreSerializableSelector();
assert.deepEqual(
select(
[
workerRecord(1, { captureGeneration: 6 }),
workerRecord(2, { kind: 'playlist-refresh.worker' }),
],
7
),
{
selected: null,
unavailableReason: 'database-worker-missing',
}
);
});
test('fails closed for multiple current-generation database workers instead of taking the first', async () => {
const select = await restoreSerializableSelector();
const first = workerRecord(1, {
finalized: false,
sampleTimer: {},
});
const second = workerRecord(2, {
finalized: true,
sampleTimer: null,
});
assert.deepEqual(select([first, second], 7), {
selected: null,
unavailableReason: 'multiple-database-workers',
});
});
test('fails closed while the only current-generation database worker is not idle', async () => {
const select = await restoreSerializableSelector();
assert.deepEqual(select([workerRecord(3, { pendingCount: 2 })], 7), {
selected: null,
unavailableReason: 'database-worker-not-idle',
});
});
@@ -0,0 +1,70 @@
export type DatabaseWorkerPostGcUnavailableReason =
| 'database-worker-missing'
| 'database-worker-not-idle'
| 'multiple-database-workers';
export interface DatabaseWorkerPostGcSelectionRecord {
readonly captureGeneration: number | null;
readonly kind: string;
readonly ordinal: number;
readonly pendingCount: number;
}
export interface DatabaseWorkerPostGcSelection<T> {
readonly selected: T | null;
readonly unavailableReason: DatabaseWorkerPostGcUnavailableReason | null;
}
export interface DatabaseWorkerPostGcSelectionApi {
select<T extends DatabaseWorkerPostGcSelectionRecord>(
records: readonly T[],
activeGeneration: number
): DatabaseWorkerPostGcSelection<T>;
}
export function createDatabaseWorkerPostGcSelectionApi(): DatabaseWorkerPostGcSelectionApi {
const helpers = {
select<T extends DatabaseWorkerPostGcSelectionRecord>(
records: readonly T[],
activeGeneration: number
): DatabaseWorkerPostGcSelection<T> {
const currentDatabaseWorkers: T[] = [];
for (const record of records) {
if (
record.captureGeneration === activeGeneration &&
record.kind === 'database.worker'
) {
currentDatabaseWorkers.push(record);
}
}
if (currentDatabaseWorkers.length === 0) {
return Object.freeze({
selected: null,
unavailableReason: 'database-worker-missing',
});
}
if (currentDatabaseWorkers.length > 1) {
return Object.freeze({
selected: null,
unavailableReason: 'multiple-database-workers',
});
}
const selected = currentDatabaseWorkers[0] as T;
if (selected.pendingCount > 0) {
return Object.freeze({
selected: null,
unavailableReason: 'database-worker-not-idle',
});
}
return Object.freeze({
selected,
unavailableReason: null,
});
},
};
return Object.freeze({ select: helpers.select });
}
@@ -0,0 +1,392 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
type IterationKind = 'diagnostic' | 'measured' | 'warmup';
type WorkerKind = 'database.worker' | 'playlist-refresh.worker';
interface TestWorkerCapture {
readonly kind: WorkerKind;
readonly postGcHeapUnavailableReason: string | null;
readonly postGcHeapUsedBytes: number | null;
}
interface TestIteration {
readonly cancellationEffectObserved: boolean;
readonly kind: IterationKind;
readonly main: {
readonly timeline: readonly {
readonly type: string;
}[];
readonly workers: readonly TestWorkerCapture[];
};
readonly runId: string;
}
interface InvalidMeasuredRun {
readonly databaseWorkerCount: number;
readonly reason: string;
readonly runId: string;
}
interface DatabaseWorkerPostGcValidity {
readonly applicableMeasuredRunCount: number;
readonly invalidMeasuredRuns: readonly InvalidMeasuredRun[];
readonly measuredRunCount: number;
readonly notApplicableMeasuredRuns: readonly {
readonly reason: string;
readonly runId: string;
}[];
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
interface ValidityModule {
assessDatabaseWorkerPostGcValidity?: (
iterations: readonly TestIteration[]
) => DatabaseWorkerPostGcValidity;
}
const validityModulePromise = import(
new URL('./database-worker-post-gc-validity.ts', import.meta.url).href
)
.then((module) => module as ValidityModule)
.catch(() => null);
function worker(
postGcHeapUsedBytes: number | null,
postGcHeapUnavailableReason: string | null,
kind: WorkerKind = 'database.worker'
): TestWorkerCapture {
return {
kind,
postGcHeapUnavailableReason,
postGcHeapUsedBytes,
};
}
function iteration(
runId: string,
kind: IterationKind,
workers: readonly TestWorkerCapture[],
timeline: readonly { readonly type: string }[] = [],
cancellationEffectObserved = false
): TestIteration {
return {
cancellationEffectObserved,
kind,
main: { timeline, workers },
runId,
};
}
test('validates database post-GC heap per measured iteration without losing raw reasons', async () => {
const module = await validityModulePromise;
assert.ok(module, 'database worker post-GC validity helper must exist');
const assess = module.assessDatabaseWorkerPostGcValidity;
assert.equal(typeof assess, 'function');
const valid = assess([
iteration('warmup-broken', 'warmup', []),
iteration('diagnostic-broken', 'diagnostic', [
worker(null, 'gc-unavailable'),
worker(100, null),
]),
iteration('measured-valid', 'measured', [
worker(4_096, null),
worker(
null,
'worker-force-terminated-before-gc',
'playlist-refresh.worker'
),
]),
]);
assert.deepEqual(valid, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: true,
validForComparison: true,
validMeasuredRunCount: 1,
});
const compensatingCardinality = assess([
iteration('measured-duplicate', 'measured', [
worker(1_024, null),
worker(2_048, null),
]),
iteration(
'measured-missing',
'measured',
[worker(3_072, null, 'playlist-refresh.worker')],
[{ type: 'db-request' }]
),
]);
assert.deepEqual(compensatingCardinality, {
applicableMeasuredRunCount: 2,
invalidMeasuredRuns: [
{
databaseWorkerCount: 2,
reason: 'multiple-database-workers',
runId: 'measured-duplicate',
},
{
databaseWorkerCount: 0,
reason: 'database-worker-missing',
runId: 'measured-missing',
},
],
measuredRunCount: 2,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
const unavailableWorker = worker(null, 'post-gc-probe-timeout');
const unavailableAndIncoherent = assess([
iteration('measured-unavailable', 'measured', [unavailableWorker]),
iteration('measured-incoherent', 'measured', [
worker(8_192, 'gc-unavailable'),
]),
]);
assert.deepEqual(unavailableAndIncoherent, {
applicableMeasuredRunCount: 2,
invalidMeasuredRuns: [
{
databaseWorkerCount: 1,
reason: 'post-gc-probe-timeout',
runId: 'measured-unavailable',
},
{
databaseWorkerCount: 1,
reason: 'post-gc-capture-invalid',
runId: 'measured-incoherent',
},
],
measuredRunCount: 2,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
assert.equal(
unavailableWorker.postGcHeapUnavailableReason,
'post-gc-probe-timeout'
);
});
test('marks parsing cancellation before persistence as DB N/A without accepting late or missing captured DB work', async () => {
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessDatabaseWorkerPostGcValidity;
assert.equal(typeof assess, 'function');
const noDatabasePhase = assess([
iteration(
'run-01',
'measured',
[
worker(
null,
'worker-force-terminated-before-gc',
'playlist-refresh.worker'
),
],
[],
true
),
iteration(
'run-02',
'measured',
[
worker(
null,
'worker-force-terminated-before-gc',
'playlist-refresh.worker'
),
],
[],
true
),
]);
assert.deepEqual(noDatabasePhase, {
applicableMeasuredRunCount: 0,
invalidMeasuredRuns: [],
measuredRunCount: 2,
notApplicableMeasuredRuns: [
{
reason: 'operation-cancelled-before-database-phase',
runId: 'run-01',
},
{
reason: 'operation-cancelled-before-database-phase',
runId: 'run-02',
},
],
validForBenchmark: true,
validForComparison: false,
validMeasuredRunCount: 0,
});
const mixedApplicability = assess([
iteration('run-cancelled', 'measured', [], [], true),
iteration('run-persisted', 'measured', [worker(4_096, null)]),
]);
assert.deepEqual(mixedApplicability, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [],
measuredRunCount: 2,
notApplicableMeasuredRuns: [
{
reason: 'operation-cancelled-before-database-phase',
runId: 'run-cancelled',
},
],
validForBenchmark: true,
validForComparison: false,
validMeasuredRunCount: 1,
});
const lateDatabaseWork = assess([
iteration(
'run-late',
'measured',
[],
[{ type: 'db-request-after-capture-cutoff' }],
true
),
]);
assert.deepEqual(lateDatabaseWork, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 0,
reason: 'database-worker-activity-after-cutoff',
runId: 'run-late',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
const unrelatedDatabaseWorker = assess([
iteration(
'run-unrelated-db',
'measured',
[worker(4_096, null)],
[{ type: 'db-request' }],
true
),
]);
assert.deepEqual(unrelatedDatabaseWorker, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 1,
reason: 'database-worker-unexpected-activity',
runId: 'run-unrelated-db',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
const nonCancelLateRequest = assess([
iteration(
'run-non-cancel-late',
'measured',
[worker(8_192, null)],
[{ type: 'db-request-after-capture-cutoff' }]
),
]);
assert.deepEqual(nonCancelLateRequest, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 1,
reason: 'database-worker-activity-after-cutoff',
runId: 'run-non-cancel-late',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
});
test('the benchmark preserves raw artifacts before rejecting an invalid formal comparison', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const validityGuard = source.indexOf(
'summary.validity.databaseWorkerPostGc.validForBenchmark'
);
const cancellationEffectGuard = source.indexOf(
'summary.cancellationEffectRate !== 1'
);
assert.ok(summaryWrite >= 0, 'summary artifact write must exist');
assert.ok(
cancellationEffectGuard >= 0,
'formal cancellation-effect guard must exist'
);
assert.ok(validityGuard >= 0, 'formal validity guard must exist');
assert.ok(
summaryWrite < cancellationEffectGuard && summaryWrite < validityGuard,
'raw summary must be durable before either formal guard fails'
);
assert.match(source, /Cancellation effect was not observed/);
assert.match(source, /Database worker post-GC capture is invalid/);
});
test('the benchmark persists the real seed DB lifecycle and atomically arms the measured generation', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const seedCaptureStart = source.indexOf(
'await startMainCapture(app.electronApp',
source.indexOf('const rendererWindowIdentity')
);
const seedPlaylist = source.indexOf('await seedPlaylist');
const seedSettlement = source.indexOf('await waitForSeedSettlement');
const seedRollover = source.indexOf(
'await rolloverMainCapture(app.electronApp',
seedSettlement
);
const seedArtifactWrite = source.indexOf(
"'seed-main-capture.json'",
seedRollover
);
assert.ok(seedCaptureStart >= 0 && seedCaptureStart < seedPlaylist);
assert.ok(seedPlaylist < seedSettlement);
assert.ok(seedSettlement < seedRollover);
assert.ok(seedRollover < seedArtifactWrite);
assert.doesNotMatch(
source.slice(seedRollover + 1, source.indexOf('const renderer =')),
/await startMainCapture\(app\.electronApp/
);
assert.match(source, /seedRollover\.nextCaptureStarted/);
assert.match(source, /seedRollover\.nextCaptureUnavailableReason/);
assert.match(
source,
/status\.databaseRequests\s*>\s*0[\s\S]*status\.databaseUpsertsCompleted\s*>\s*0[\s\S]*status\.databasePending\s*===\s*0/
);
});
@@ -0,0 +1,141 @@
import {
type DatabaseWorkerPostGcValidity,
type InvalidDatabaseWorkerPostGcMeasuredRun,
WORKER_POST_GC_HEAP_UNAVAILABLE_REASON,
} from './m3u-refresh-cancellation-contract';
export interface DatabaseWorkerPostGcValidityWorker {
readonly kind: string;
readonly postGcHeapUnavailableReason: unknown;
readonly postGcHeapUsedBytes: unknown;
}
export interface DatabaseWorkerPostGcValidityIteration {
readonly cancellationEffectObserved?: boolean;
readonly kind: string;
readonly main: {
readonly timeline?: readonly {
readonly type: string;
}[];
readonly workers: readonly DatabaseWorkerPostGcValidityWorker[];
};
readonly runId: string;
}
const UNAVAILABLE_REASONS = new Set<string>(
Object.values(WORKER_POST_GC_HEAP_UNAVAILABLE_REASON)
);
export function assessDatabaseWorkerPostGcValidity(
iterations: readonly DatabaseWorkerPostGcValidityIteration[]
): DatabaseWorkerPostGcValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === 'measured'
);
const invalidMeasuredRuns: InvalidDatabaseWorkerPostGcMeasuredRun[] = [];
const notApplicableMeasuredRuns: {
readonly reason: 'operation-cancelled-before-database-phase';
readonly runId: string;
}[] = [];
let applicableMeasuredRunCount = 0;
let validMeasuredRunCount = 0;
for (const iteration of measured) {
const databaseWorkers = iteration.main.workers.filter(
(worker) => worker.kind === 'database.worker'
);
const timeline = iteration.main.timeline ?? [];
const lateDatabaseRequest = timeline.some(
(record) => record.type === 'db-request-after-capture-cutoff'
);
const databasePhaseObserved =
lateDatabaseRequest ||
timeline.some((record) => record.type === 'db-request');
if (lateDatabaseRequest) {
applicableMeasuredRunCount += 1;
invalidMeasuredRuns.push({
databaseWorkerCount: databaseWorkers.length,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.DATABASE_WORKER_ACTIVITY_AFTER_CUTOFF,
runId: iteration.runId,
});
continue;
}
if (iteration.cancellationEffectObserved === true) {
if (!databasePhaseObserved && databaseWorkers.length === 0) {
notApplicableMeasuredRuns.push({
reason: 'operation-cancelled-before-database-phase',
runId: iteration.runId,
});
continue;
}
applicableMeasuredRunCount += 1;
invalidMeasuredRuns.push({
databaseWorkerCount: databaseWorkers.length,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.DATABASE_WORKER_UNEXPECTED_ACTIVITY,
runId: iteration.runId,
});
continue;
}
if (databaseWorkers.length === 0) {
applicableMeasuredRunCount += 1;
invalidMeasuredRuns.push({
databaseWorkerCount: 0,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.DATABASE_WORKER_MISSING,
runId: iteration.runId,
});
continue;
}
applicableMeasuredRunCount += 1;
if (databaseWorkers.length !== 1) {
invalidMeasuredRuns.push({
databaseWorkerCount: databaseWorkers.length,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.MULTIPLE_DATABASE_WORKERS,
runId: iteration.runId,
});
continue;
}
const worker = databaseWorkers[0] as DatabaseWorkerPostGcValidityWorker;
const heap = worker.postGcHeapUsedBytes;
const reason = worker.postGcHeapUnavailableReason;
if (
Number.isSafeInteger(heap) &&
Number(heap) >= 0 &&
reason === null
) {
validMeasuredRunCount += 1;
continue;
}
invalidMeasuredRuns.push({
databaseWorkerCount: 1,
reason:
heap === null &&
typeof reason === 'string' &&
UNAVAILABLE_REASONS.has(reason)
? reason
: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.INVALID_CAPTURE,
runId: iteration.runId,
});
}
return Object.freeze({
applicableMeasuredRunCount,
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
notApplicableMeasuredRuns: Object.freeze(notApplicableMeasuredRuns),
validForBenchmark:
measured.length > 0 && invalidMeasuredRuns.length === 0,
validForComparison:
measured.length > 0 &&
applicableMeasuredRunCount === measured.length &&
validMeasuredRunCount === measured.length &&
invalidMeasuredRuns.length === 0,
validMeasuredRunCount,
});
}
@@ -0,0 +1,193 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
interface DiagnosticCaptureModule {
validateDiagnosticPlaylistWorkerCapture?: (
main: MainCaptureMetrics,
iterationDirectory: string
) => Promise<void>;
}
const diagnosticCaptureModulePromise = import(
new URL('./diagnostic-playlist-worker-capture.ts', import.meta.url).href
)
.then((module) => module as DiagnosticCaptureModule)
.catch(() => null);
function mainCapture(input: {
readonly profilePath: string | null;
readonly snapshotPath?: string | null;
readonly timelineType?: string;
}): MainCaptureMetrics {
return {
timeline: input.timelineType
? [{ epochMs: 1, type: input.timelineType }]
: [],
workers: [
{
kind: 'playlist-refresh.worker',
ordinal: 2,
postGcHeapUnavailableReason:
'worker-force-terminated-before-gc',
postGcHeapUsedBytes: null,
profilePath: input.profilePath,
snapshotPath: input.snapshotPath ?? null,
terminatedEpochMs: 2,
},
],
} as unknown as MainCaptureMetrics;
}
test('accepts one parseable diagnostic playlist-worker profile inside the iteration directory', async () => {
const module = await diagnosticCaptureModulePromise;
assert.ok(
module,
'diagnostic playlist worker capture validator must exist'
);
const validate = module.validateDiagnosticPlaylistWorkerCapture;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-diagnostic-worker-')
);
try {
const profilePath = join(
directory,
'playlist-refresh.worker-2.cpuprofile'
);
await writeFile(
profilePath,
JSON.stringify({ nodes: [{ id: 1 }], samples: [1] })
);
await assert.doesNotReject(() =>
validate?.(mainCapture({ profilePath }), directory)
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('fails closed for missing, escaped, malformed, or contaminated diagnostic artifacts', async () => {
const module = await diagnosticCaptureModulePromise;
assert.ok(module);
const validate = module.validateDiagnosticPlaylistWorkerCapture;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-diagnostic-worker-invalid-')
);
try {
await assert.rejects(
() => validate?.(mainCapture({ profilePath: null }), directory),
/diagnostic-playlist-worker-profile-missing/
);
await assert.rejects(
() =>
validate?.(
mainCapture({
profilePath: join(
directory,
'..',
'escaped.cpuprofile'
),
}),
directory
),
/diagnostic-playlist-worker-profile-path-invalid/
);
const malformedPath = join(
directory,
'playlist-refresh.worker-2.cpuprofile'
);
await writeFile(malformedPath, '{"nodes":[]}');
await assert.rejects(
() =>
validate?.(
mainCapture({ profilePath: malformedPath }),
directory
),
/diagnostic-playlist-worker-profile-invalid/
);
await assert.rejects(
() =>
validate?.(
mainCapture({
profilePath: malformedPath,
timelineType:
'worker-artifact-error:profile-stop:failed',
}),
directory
),
/diagnostic-playlist-worker-artifact-error/
);
await assert.rejects(
() =>
validate?.(
mainCapture({
profilePath: malformedPath,
snapshotPath: join(
directory,
'playlist-refresh.worker-2.heapsnapshot'
),
}),
directory
),
/diagnostic-playlist-worker-snapshot-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('the benchmark persists diagnostic raw metrics before requiring a parseable playlist-worker profile', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const resultWrite = source.indexOf(
"writeJson(join(iterationDirectory, 'result.json'), result)"
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const diagnosticValidation = source.indexOf(
'await validateDiagnosticPlaylistWorkerCapture('
);
assert.ok(resultWrite >= 0);
assert.ok(summaryWrite >= 0);
assert.ok(diagnosticValidation > summaryWrite);
});
test('the benchmark requires the diagnostic worker profile to come from an observed cancellation', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const diagnosticCancellationGuard = source.indexOf(
'if (!diagnosticIteration.cancellationEffectObserved)'
);
const diagnosticValidation = source.indexOf(
'await validateDiagnosticPlaylistWorkerCapture('
);
assert.ok(summaryWrite >= 0);
assert.ok(
diagnosticCancellationGuard > summaryWrite,
'raw manifest and summary must remain durable when the diagnostic cancellation is invalid'
);
assert.ok(
diagnosticCancellationGuard < diagnosticValidation,
'the cancellation guard must reject a normal-completion worker profile before artifact validation'
);
});
@@ -0,0 +1,72 @@
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
interface CpuProfile {
readonly nodes?: unknown;
readonly samples?: unknown;
}
export async function validateDiagnosticPlaylistWorkerCapture(
main: MainCaptureMetrics,
iterationDirectory: string
): Promise<void> {
if (
main.timeline.some((record) =>
record.type.startsWith('worker-artifact-error:')
)
) {
throw new Error('diagnostic-playlist-worker-artifact-error');
}
const workers = main.workers.filter(
(worker) => worker.kind === 'playlist-refresh.worker'
);
if (workers.length !== 1) {
throw new Error('diagnostic-playlist-worker-cardinality-invalid');
}
const worker = workers[0];
if (!worker) {
throw new Error('diagnostic-playlist-worker-cardinality-invalid');
}
if (worker.snapshotPath !== null) {
throw new Error('diagnostic-playlist-worker-snapshot-unexpected');
}
if (
worker.postGcHeapUsedBytes !== null ||
worker.postGcHeapUnavailableReason !==
'worker-force-terminated-before-gc' ||
worker.terminatedEpochMs === null
) {
throw new Error('diagnostic-playlist-worker-termination-invalid');
}
if (worker.profilePath === null) {
throw new Error('diagnostic-playlist-worker-profile-missing');
}
const expectedProfilePath = join(
resolve(iterationDirectory),
`playlist-refresh.worker-${worker.ordinal}.cpuprofile`
);
if (resolve(worker.profilePath) !== expectedProfilePath) {
throw new Error('diagnostic-playlist-worker-profile-path-invalid');
}
let profile: CpuProfile;
try {
profile = JSON.parse(
await readFile(expectedProfilePath, 'utf8')
) as CpuProfile;
} catch {
throw new Error('diagnostic-playlist-worker-profile-invalid');
}
if (
!Array.isArray(profile.nodes) ||
profile.nodes.length === 0 ||
!Array.isArray(profile.samples) ||
profile.samples.length === 0
) {
throw new Error('diagnostic-playlist-worker-profile-invalid');
}
}
@@ -1,3 +1,8 @@
import type {
RendererProcessRssCapture,
RendererProcessRssUnavailableReason,
} from './renderer-process-rss-capture';
export const PERFORMANCE_ITERATION_KIND = {
DIAGNOSTIC: 'diagnostic',
MEASURED: 'measured',
@@ -15,6 +20,40 @@ export const PERFORMANCE_WORKER_KIND = {
export type PerformanceWorkerKind =
(typeof PERFORMANCE_WORKER_KIND)[keyof typeof PERFORMANCE_WORKER_KIND];
export const WORKER_POST_GC_HEAP_UNAVAILABLE_REASON = {
CAPTURE_FAILED: 'capture-failed',
DATABASE_WORKER_ACTIVITY_AFTER_CUTOFF:
'database-worker-activity-after-cutoff',
DATABASE_WORKER_MISSING: 'database-worker-missing',
DATABASE_WORKER_NOT_IDLE: 'database-worker-not-idle',
DATABASE_WORKER_UNEXPECTED_ACTIVITY: 'database-worker-unexpected-activity',
GC_UNAVAILABLE: 'gc-unavailable',
INVALID_CAPTURE: 'post-gc-capture-invalid',
MULTIPLE_DATABASE_WORKERS: 'multiple-database-workers',
PROBE_INVALID_RESPONSE: 'post-gc-probe-invalid-response',
PROBE_MESSAGE_ERROR: 'post-gc-probe-message-error',
PROBE_NOT_RUN: 'post-gc-probe-not-run',
PROBE_PORT_CLOSED: 'post-gc-probe-port-closed',
PROBE_POST_FAILED: 'post-gc-probe-post-failed',
PROBE_TIMEOUT: 'post-gc-probe-timeout',
PROFILING_DISABLED: 'profiling-disabled',
WORKER_BUSY: 'worker-busy',
WORKER_FORCE_TERMINATED: 'worker-force-terminated-before-gc',
} as const;
export type WorkerPostGcHeapUnavailableReason =
(typeof WORKER_POST_GC_HEAP_UNAVAILABLE_REASON)[keyof typeof WORKER_POST_GC_HEAP_UNAVAILABLE_REASON];
export type WorkerPostGcHeapCapture =
| {
readonly postGcHeapUnavailableReason: null;
readonly postGcHeapUsedBytes: number;
}
| {
readonly postGcHeapUnavailableReason: WorkerPostGcHeapUnavailableReason;
readonly postGcHeapUsedBytes: null;
};
export interface NumericDistribution {
readonly count: number;
readonly max: number | null;
@@ -71,7 +110,7 @@ export interface WorkerRequestPerformanceMetrics {
readonly workStartedEpochMs: number | null;
}
export interface WorkerCaptureMetrics {
export type WorkerCaptureMetrics = {
readonly cancelPostedEpochMs: number | null;
readonly cpuSystemMicros: number | null;
readonly cpuUserMicros: number | null;
@@ -80,16 +119,16 @@ export interface WorkerCaptureMetrics {
readonly eventLoopUtilization: number | null;
readonly kind: PerformanceWorkerKind;
readonly operationId: string | null;
readonly ordinal: number;
readonly peakExternalBytes: number;
readonly peakHeapUsedBytes: number;
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;
}
} & WorkerPostGcHeapCapture;
export interface MainCaptureMetrics {
readonly cpuProfilePath: string | null;
@@ -100,11 +139,17 @@ export interface MainCaptureMetrics {
readonly eventLoopUtilizationUnavailableReason: string | null;
readonly heapSnapshotPath: string | null;
readonly memory: ProcessMemoryMetrics;
readonly rendererPeakRssBytes: number;
readonly rendererWindow: {
readonly responsiveEvents: number;
readonly rss: RendererProcessRssCapture;
readonly unresponsiveEvents: number;
readonly windowIdentity: {
readonly browserWindowId: number;
readonly webContentsId: number;
};
};
readonly rssScope: 'electron-main-process-including-worker-threads-and-native-memory';
readonly timeline: readonly MainTimelineRecord[];
readonly unresponsiveEvents: number;
readonly responsiveEvents: number;
readonly workers: readonly WorkerCaptureMetrics[];
}
@@ -191,6 +236,43 @@ export interface CancellationBenchmarkManifest {
readonly warmupRuns: number;
}
export interface InvalidDatabaseWorkerPostGcMeasuredRun {
readonly databaseWorkerCount: number;
readonly reason: string;
readonly runId: string;
}
export interface NotApplicableDatabaseWorkerPostGcMeasuredRun {
readonly reason: 'operation-cancelled-before-database-phase';
readonly runId: string;
}
export interface DatabaseWorkerPostGcValidity {
readonly applicableMeasuredRunCount: number;
readonly invalidMeasuredRuns: readonly InvalidDatabaseWorkerPostGcMeasuredRun[];
readonly measuredRunCount: number;
readonly notApplicableMeasuredRuns: readonly NotApplicableDatabaseWorkerPostGcMeasuredRun[];
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
export interface InvalidRendererRssMeasuredRun {
readonly missingSampleCount: number;
readonly reason:
RendererProcessRssUnavailableReason | 'renderer-rss-capture-invalid';
readonly runId: string;
readonly validSampleCount: number;
}
export interface RendererRssValidity {
readonly invalidMeasuredRuns: readonly InvalidRendererRssMeasuredRun[];
readonly measuredRunCount: number;
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
export interface CancellationBenchmarkSummary {
readonly cancellationEffectRate: number;
readonly iterations: readonly CancellationIterationResult[];
@@ -244,4 +326,8 @@ export interface CancellationBenchmarkSummary {
readonly unresponsiveEvents: number;
readonly visibleTotalMs: NumericDistribution;
};
readonly validity: {
readonly databaseWorkerPostGc: DatabaseWorkerPostGcValidity;
readonly rendererRss: RendererRssValidity;
};
}
@@ -9,6 +9,7 @@ import type {
NumericDistribution,
PerformanceWorkerKind,
RendererCaptureMetrics,
RendererRssValidity,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
@@ -19,6 +20,7 @@ import {
nonNegativeDifference,
summarizeNumbers,
} from './performance-statistics';
import { assessDatabaseWorkerPostGcValidity } from './database-worker-post-gc-validity';
export function createCancellationIterationResult(input: {
readonly kind: CancellationIterationResult['kind'];
@@ -57,12 +59,11 @@ export function createCancellationBenchmarkSummary(
select: (worker: MainCaptureMetrics['workers'][number]) => number | null
): NumericDistribution =>
summarizeNumbers(
measured.map((iteration) => {
const worker = iteration.main.workers.find(
(candidate) => candidate.kind === kind
);
return worker ? select(worker) : null;
})
measured.flatMap((iteration) =>
iteration.main.workers
.filter((worker) => worker.kind === kind)
.map((worker) => select(worker))
)
);
const workerRequestMetric = (
kind: PerformanceWorkerKind,
@@ -280,10 +281,14 @@ export function createCancellationBenchmarkSummary(
)
),
rendererRssPeakBytes: summarizeNumbers(
measured.map((iteration) => iteration.main.rendererPeakRssBytes)
measured.map(
(iteration) =>
iteration.main.rendererWindow.rss.peakRssBytes
)
),
responsiveEvents: measured.reduce(
(sum, iteration) => sum + iteration.main.responsiveEvents,
(sum, iteration) =>
sum + iteration.main.rendererWindow.responsiveEvents,
0
),
totalMs: summarizeNumbers(
@@ -295,13 +300,63 @@ export function createCancellationBenchmarkSummary(
)
),
unresponsiveEvents: measured.reduce(
(sum, iteration) => sum + iteration.main.unresponsiveEvents,
(sum, iteration) =>
sum + iteration.main.rendererWindow.unresponsiveEvents,
0
),
visibleTotalMs: summarizeNumbers(
measured.map((iteration) => iteration.phases.visibleTotalMs)
),
}),
validity: Object.freeze({
databaseWorkerPostGc:
assessDatabaseWorkerPostGcValidity(iterations),
rendererRss: assessRendererRssValidity(iterations),
}),
});
}
function assessRendererRssValidity(
iterations: readonly CancellationIterationResult[]
): RendererRssValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
);
const invalidMeasuredRuns: RendererRssValidity['invalidMeasuredRuns'][number][] =
[];
let validMeasuredRunCount = 0;
for (const iteration of measured) {
const rss = iteration.main.rendererWindow.rss;
if (
rss.identity !== null &&
Number.isSafeInteger(rss.peakRssBytes) &&
Number(rss.peakRssBytes) > 0 &&
rss.unavailableReason === null &&
rss.missingSampleCount === 0 &&
Number.isSafeInteger(rss.validSampleCount) &&
rss.validSampleCount > 0
) {
validMeasuredRunCount += 1;
continue;
}
invalidMeasuredRuns.push({
missingSampleCount: rss.missingSampleCount,
reason: rss.unavailableReason ?? 'renderer-rss-capture-invalid',
runId: iteration.runId,
validSampleCount: rss.validSampleCount,
});
}
const valid =
measured.length > 0 && validMeasuredRunCount === measured.length;
return Object.freeze({
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
validForBenchmark: valid,
validForComparison: valid,
validMeasuredRunCount,
});
}
@@ -35,13 +35,17 @@ import {
createCancellationBenchmarkSummary,
createCancellationIterationResult,
} from './m3u-refresh-cancellation-report';
import { validateDiagnosticPlaylistWorkerCapture } from './diagnostic-playlist-worker-capture';
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
import {
installMainCapture,
readMainCaptureStatus,
rolloverMainCapture,
startMainCapture,
stopMainCapture,
} from './m3u-refresh-main-capture';
import { startRendererCapture } from './m3u-refresh-renderer-capture';
import type { RendererWindowIdentity } from './renderer-window-rss-session';
import {
createSyntheticM3uFixture,
SYNTHETIC_M3U_CHANNEL_COUNT,
@@ -133,6 +137,40 @@ export async function runM3uRefreshCancellationBenchmark(): Promise<void> {
const summary = createCancellationBenchmarkSummary(manifest, iterations);
await writeJson(join(config.outputDirectory, 'manifest.json'), manifest);
await writeJson(join(config.outputDirectory, 'summary.json'), summary);
const diagnosticIteration = iterations.find(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC
);
if (!diagnosticIteration) {
throw new Error('Diagnostic iteration is missing');
}
if (!diagnosticIteration.cancellationEffectObserved) {
throw new Error(
'Cancellation effect was not observed in the diagnostic run'
);
}
await validateDiagnosticPlaylistWorkerCapture(
diagnosticIteration.main,
join(config.outputDirectory, diagnosticIteration.runId)
);
if (summary.cancellationEffectRate !== 1) {
throw new Error(
`Cancellation effect was not observed in every measured run: ${summary.cancellationEffectRate}`
);
}
if (!summary.validity.rendererRss.validForBenchmark) {
throw new Error(
`Renderer RSS capture is invalid: ${JSON.stringify(
summary.validity.rendererRss.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerPostGc.validForBenchmark) {
throw new Error(
`Database worker post-GC capture is invalid: ${JSON.stringify(
summary.validity.databaseWorkerPostGc.invalidMeasuredRuns
)}`
);
}
console.log(
`[performance] completed: ${join(
config.outputDirectory,
@@ -147,10 +185,11 @@ async function runIteration(
server: SyntheticServer
): Promise<CancellationIterationResult> {
const iterationDirectory = join(config.outputDirectory, definition.runId);
await assertPerformanceArtifactCapacity(iterationDirectory);
await mkdir(iterationDirectory, { recursive: false });
const dataDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-m3u-performance-')
);
await mkdir(iterationDirectory, { recursive: false });
let app: LaunchedElectronApp | null = null;
let rendererConsoleListener: ((message: ConsoleMessage) => void) | null =
null;
@@ -160,6 +199,7 @@ async function runIteration(
server.serveSeedFixture();
app = await launchElectronApp(dataDirectory, {
args: [
'--js-flags=--expose-gc',
'--remote-debugging-address=127.0.0.1',
`--remote-debugging-port=${RENDERER_CDP_PORT}`,
`--user-data-dir=${join(dataDirectory, 'user-data')}`,
@@ -175,6 +215,12 @@ async function runIteration(
await assertRendererCdpTarget(app.mainWindow);
app.mainWindow.setDefaultTimeout(120_000);
await installMainCapture(app.electronApp);
const rendererWindowIdentity = await resolveRendererWindowIdentity(app);
await startMainCapture(app.electronApp, {
diagnostic: false,
outputDirectory: iterationDirectory,
rendererWindowIdentity,
});
await seedPlaylist(app.mainWindow, server.resourceUrl);
await waitForSeedSettlement(app);
server.serveLargeFixture();
@@ -191,10 +237,20 @@ async function runIteration(
const diagnostic =
definition.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC;
await startMainCapture(app.electronApp, {
const seedRollover = await rolloverMainCapture(app.electronApp, {
diagnostic,
outputDirectory: iterationDirectory,
rendererWindowIdentity,
});
await writeJson(
join(iterationDirectory, 'seed-main-capture.json'),
seedRollover.completedCapture
);
if (!seedRollover.nextCaptureStarted) {
throw new Error(
`Seed capture could not atomically arm the measured generation: ${seedRollover.nextCaptureUnavailableReason}`
);
}
const renderer = await startRendererCapture(app.mainWindow, {
diagnostic,
outputDirectory: iterationDirectory,
@@ -236,6 +292,28 @@ async function runIteration(
}
}
async function resolveRendererWindowIdentity(
app: LaunchedElectronApp
): Promise<RendererWindowIdentity> {
const browserWindowHandle = await app.electronApp.browserWindow(
app.mainWindow
);
try {
return await browserWindowHandle.evaluate((browserWindow) => {
const exactWindow = browserWindow as unknown as {
readonly id: number;
readonly webContents: { readonly id: number };
};
return {
browserWindowId: exactWindow.id,
webContentsId: exactWindow.webContents.id,
};
});
} finally {
await browserWindowHandle.dispose();
}
}
async function seedPlaylist(page: Page, playlistUrl: string): Promise<void> {
await page.getByRole('button', { name: 'Add playlist' }).first().click();
const dialog = page.locator('mat-dialog-container').last();
@@ -259,11 +337,17 @@ async function seedPlaylist(page: Page, playlistUrl: string): Promise<void> {
async function waitForSeedSettlement(app: LaunchedElectronApp): Promise<void> {
await expect
.poll(
async () =>
(await readMainCaptureStatus(app.electronApp)).databasePending,
async () => {
const status = await readMainCaptureStatus(app.electronApp);
return (
status.databaseRequests > 0 &&
status.databaseUpsertsCompleted > 0 &&
status.databasePending === 0
);
},
{ timeout: 120_000 }
)
.toBe(0);
.toBe(true);
await waitForTwoAnimationFrames(app.mainWindow);
}
@@ -363,6 +447,7 @@ async function resolveConfiguration(): Promise<BenchmarkConfiguration> {
}
const outputDirectory = join(outputRoot, variant);
await assertMissing(outputDirectory);
await assertPerformanceArtifactCapacity(outputDirectory);
await mkdir(outputDirectory, { recursive: true });
return Object.freeze({
electronVersion: electronPackage.version,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,124 @@
/* 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 { dirname, resolve } from 'node:path';
import {
assertPerformanceArtifactCapacity,
MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
} from './performance-artifact-preflight';
test('checks the nearest existing ancestor using available blocks', async () => {
const requestedPath = resolve(
'/workspace/dist/performance/2026-07-26/baseline'
);
const checkedPaths: string[] = [];
const existingAncestor = dirname(dirname(requestedPath));
const result = await assertPerformanceArtifactCapacity(requestedPath, {
statfs: async (path) => {
checkedPaths.push(path);
if (path !== existingAncestor) {
throw Object.assign(new Error('missing'), { code: 'ENOENT' });
}
return {
bavail: 4_194_304n,
bsize: 1_024n,
};
},
});
assert.deepEqual(checkedPaths, [
requestedPath,
dirname(requestedPath),
existingAncestor,
]);
assert.deepEqual(result, {
availableBytes: 4_294_967_296n,
checkedPath: existingAncestor,
minimumBytes: MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
});
});
test('accepts exactly two GiB of available capacity', async () => {
const result = await assertPerformanceArtifactCapacity('/existing', {
statfs: async () => ({
bavail: 2_097_152n,
bsize: 1_024n,
}),
});
assert.equal(result.availableBytes, MIN_PERFORMANCE_ARTIFACT_FREE_BYTES);
});
test('fails before artifact creation when available capacity is below two GiB', async () => {
await assert.rejects(
assertPerformanceArtifactCapacity('/existing', {
statfs: async () => ({
bavail: 2_097_151n,
bsize: 1_024n,
}),
}),
/requires at least 2147483648 available bytes.*2147482624/
);
});
test('does not substitute free blocks that are unavailable to the process', async () => {
await assert.rejects(
assertPerformanceArtifactCapacity('/existing', {
statfs: async () => ({
bavail: 1n,
bfree: 9_999_999_999n,
bsize: 1_024n,
}),
}),
/performance-artifact-space-insufficient/
);
});
test('does not hide filesystem errors other than a missing path', async () => {
const denied = Object.assign(new Error('denied'), { code: 'EACCES' });
let calls = 0;
await assert.rejects(
assertPerformanceArtifactCapacity('/denied/child', {
statfs: async () => {
calls += 1;
throw denied;
},
}),
denied
);
assert.equal(calls, 1);
});
test('the benchmark preflights capacity before creating output directories', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const runIteration = source.slice(
source.indexOf('async function runIteration('),
source.indexOf('async function seedPlaylist(')
);
const resolveConfiguration = source.slice(
source.indexOf('async function resolveConfiguration('),
source.indexOf('function readGitSourceState(')
);
for (const [name, section] of [
['iteration', runIteration],
['variant', resolveConfiguration],
] as const) {
const preflight = section.indexOf(
'await assertPerformanceArtifactCapacity('
);
const createDirectory = section.indexOf('await mkdir(');
assert.ok(preflight >= 0, `${name} preflight is missing`);
assert.ok(
preflight < createDirectory,
`${name} preflight must run before mkdir`
);
}
});
@@ -0,0 +1,76 @@
import { statfs } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
export const MIN_PERFORMANCE_ARTIFACT_FREE_BYTES =
2n * 1_024n * 1_024n * 1_024n;
interface FileSystemCapacity {
readonly bavail: bigint;
readonly bsize: bigint;
}
interface PerformanceArtifactPreflightDependencies {
readonly statfs?: (path: string) => Promise<FileSystemCapacity>;
}
export interface PerformanceArtifactCapacity {
readonly availableBytes: bigint;
readonly checkedPath: string;
readonly minimumBytes: bigint;
}
async function readFileSystemCapacity(
path: string
): Promise<FileSystemCapacity> {
const capacity = await statfs(path, { bigint: true });
return {
bavail: capacity.bavail,
bsize: capacity.bsize,
};
}
function isMissingPath(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
error.code === 'ENOENT'
);
}
export async function assertPerformanceArtifactCapacity(
targetPath: string,
dependencies: PerformanceArtifactPreflightDependencies = {}
): Promise<PerformanceArtifactCapacity> {
const readCapacity = dependencies.statfs ?? readFileSystemCapacity;
let checkedPath = resolve(targetPath);
while (true) {
try {
const capacity = await readCapacity(checkedPath);
const availableBytes = capacity.bavail * capacity.bsize;
if (availableBytes < MIN_PERFORMANCE_ARTIFACT_FREE_BYTES) {
throw new Error(
`performance-artifact-space-insufficient: benchmark requires at least ${MIN_PERFORMANCE_ARTIFACT_FREE_BYTES} available bytes; ${availableBytes} available at ${checkedPath}`
);
}
return Object.freeze({
availableBytes,
checkedPath,
minimumBytes: MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
});
} catch (error) {
if (!isMissingPath(error)) {
throw error;
}
const parent = dirname(checkedPath);
if (parent === checkedPath) {
throw new Error(
`performance-artifact-space-unavailable: no existing ancestor for ${targetPath}`,
{ cause: error }
);
}
checkedPath = parent;
}
}
}
@@ -257,3 +257,16 @@ test('the cancellation benchmark enables preload performance capture', () => {
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
});
test('the cancellation benchmark exposes GC to Electron worker isolates', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation.benchmark.ts'
),
'utf8'
);
assert.match(source, /'--js-flags=--expose-gc'/);
assert.doesNotMatch(source, /execArgv/);
});
@@ -0,0 +1,309 @@
/* 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 * as rendererProcessRssCaptureModule from './renderer-process-rss-capture';
import {
captureRendererProcessRssSample,
createRendererProcessRssCapture,
type RendererProcessRssCapture,
type RendererProcessRssUnavailableReason,
} from './renderer-process-rss-capture';
const RENDERER_PID = 42;
const RENDERER_CREATION_TIME = 1_721_234_567_890;
test('exposes a self-contained factory for the Electron main-process injection boundary', () => {
const factory = (
rendererProcessRssCaptureModule as unknown as Record<string, unknown>
)['createRendererProcessRssCaptureApi'];
assert.equal(typeof factory, 'function');
const source = (factory as () => unknown).toString();
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => {
create(
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture;
};
assert.deepEqual(restoredFactory().create(RENDERER_PID, [metric()]), {
identity: {
creationTime: RENDERER_CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 1_263_616,
unavailableReason: null,
validSampleCount: 1,
});
});
interface MetricOverrides {
readonly creationTime?: unknown;
readonly pid?: unknown;
readonly type?: unknown;
readonly workingSetSize?: unknown;
}
function metric(overrides: MetricOverrides = {}): unknown {
return {
creationTime:
overrides.creationTime === undefined
? RENDERER_CREATION_TIME
: overrides.creationTime,
memory: {
workingSetSize:
overrides.workingSetSize === undefined
? 1_234
: overrides.workingSetSize,
},
pid: overrides.pid === undefined ? RENDERER_PID : overrides.pid,
type: overrides.type === undefined ? 'Tab' : overrides.type,
};
}
function assertUnavailable(
capture: RendererProcessRssCapture,
unavailableReason: RendererProcessRssUnavailableReason,
expected: {
readonly identity: RendererProcessRssCapture['identity'];
readonly missingSampleCount?: number;
readonly validSampleCount?: number;
}
): void {
assert.deepEqual(capture, {
identity: expected.identity,
missingSampleCount: expected.missingSampleCount ?? 0,
peakRssBytes: null,
unavailableReason,
validSampleCount: expected.validSampleCount ?? 0,
});
}
test('binds only the exact BrowserWindow renderer identity and converts KiB to bytes once', () => {
const capture = createRendererProcessRssCapture(RENDERER_PID, [
metric({
creationTime: RENDERER_CREATION_TIME + 1,
pid: 900,
type: 'Renderer',
workingSetSize: 9_999_999,
}),
metric(),
]);
assert.deepEqual(capture, {
identity: {
creationTime: RENDERER_CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 1_263_616,
unavailableReason: null,
validSampleCount: 1,
});
});
test('accepts only exact Tab or Renderer process types and tracks the peak without mutating prior state', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [
metric({ type: 'Tab', workingSetSize: 2_000 }),
]);
const lower = captureRendererProcessRssSample(initial, RENDERER_PID, [
metric({ type: 'Renderer', workingSetSize: 1_500 }),
metric({
pid: 777,
type: 'Renderer',
workingSetSize: 8_000_000,
}),
]);
assert.equal(initial.validSampleCount, 1);
assert.equal(initial.peakRssBytes, 2_048_000);
assert.deepEqual(lower, {
identity: {
creationTime: RENDERER_CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 2_048_000,
unavailableReason: null,
validSampleCount: 2,
});
const higher = captureRendererProcessRssSample(lower, RENDERER_PID, [
metric({ workingSetSize: 2_500 }),
]);
assert.equal(higher.peakRssBytes, 2_560_000);
assert.equal(higher.validSampleCount, 3);
});
test('fails closed when the BrowserWindow OS process id is invalid', () => {
for (const invalidPid of [0, -1, 1.5, Number.NaN, '42'] as const) {
assertUnavailable(
createRendererProcessRssCapture(invalidPid, [metric()]),
'renderer-os-pid-invalid',
{ identity: null }
);
}
});
test('fails closed when the initial target metric is missing or ambiguous', () => {
assertUnavailable(
createRendererProcessRssCapture(RENDERER_PID, [
metric({ pid: 99, workingSetSize: 99_999 }),
]),
'renderer-process-metric-missing-at-start',
{
identity: null,
missingSampleCount: 1,
}
);
assertUnavailable(
createRendererProcessRssCapture(RENDERER_PID, [
metric(),
metric({ type: 'Renderer', workingSetSize: 2_000 }),
]),
'renderer-process-metric-ambiguous',
{ identity: null }
);
});
test('fails closed for invalid initial type, creation time, or working set', () => {
const cases: ReadonlyArray<{
readonly metric: unknown;
readonly reason: RendererProcessRssUnavailableReason;
}> = [
{
metric: metric({ type: 'RendererHelper' }),
reason: 'renderer-process-metric-type-invalid',
},
{
metric: metric({ creationTime: '1721234567890' }),
reason: 'renderer-process-metric-creation-time-invalid',
},
{
metric: metric({ workingSetSize: Number.POSITIVE_INFINITY }),
reason: 'renderer-process-metric-working-set-invalid',
},
{
metric: metric({ workingSetSize: 0 }),
reason: 'renderer-process-metric-working-set-invalid',
},
];
for (const testCase of cases) {
assertUnavailable(
createRendererProcessRssCapture(RENDERER_PID, [testCase.metric]),
testCase.reason,
{ identity: null }
);
}
});
test('invalidates the capture when the bound renderer is missing and never recovers', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
const missing = captureRendererProcessRssSample(initial, RENDERER_PID, [
metric({ pid: 700, workingSetSize: 9_999_999 }),
]);
assertUnavailable(
missing,
'renderer-process-metric-missing-during-capture',
{
identity: initial.identity,
missingSampleCount: 1,
validSampleCount: 1,
}
);
const laterValid = captureRendererProcessRssSample(missing, RENDERER_PID, [
metric({ workingSetSize: 5_000 }),
]);
assert.deepEqual(laterValid, missing);
});
test('invalidates the capture when the BrowserWindow or process metric identity changes', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
assertUnavailable(
captureRendererProcessRssSample(initial, 43, [
metric({
creationTime: RENDERER_CREATION_TIME + 1,
pid: 43,
}),
]),
'renderer-process-identity-changed',
{
identity: initial.identity,
validSampleCount: 1,
}
);
assertUnavailable(
captureRendererProcessRssSample(initial, RENDERER_PID, [
metric({ creationTime: RENDERER_CREATION_TIME + 1 }),
]),
'renderer-process-identity-changed',
{
identity: initial.identity,
validSampleCount: 1,
}
);
});
test('fails closed for invalid or ambiguous metrics after binding', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
const cases: ReadonlyArray<{
readonly metrics: readonly unknown[];
readonly reason: RendererProcessRssUnavailableReason;
}> = [
{
metrics: [metric(), metric({ type: 'Renderer' })],
reason: 'renderer-process-metric-ambiguous',
},
{
metrics: [metric({ type: 'RendererHelper' })],
reason: 'renderer-process-metric-type-invalid',
},
{
metrics: [metric({ creationTime: Number.NaN })],
reason: 'renderer-process-metric-creation-time-invalid',
},
{
metrics: [metric({ workingSetSize: -1 })],
reason: 'renderer-process-metric-working-set-invalid',
},
];
for (const testCase of cases) {
assertUnavailable(
captureRendererProcessRssSample(
initial,
RENDERER_PID,
testCase.metrics
),
testCase.reason,
{
identity: initial.identity,
validSampleCount: 1,
}
);
}
});
test('keeps invalid renderer PIDs sticky after binding', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
const invalid = captureRendererProcessRssSample(initial, 0, [metric()]);
assertUnavailable(invalid, 'renderer-os-pid-invalid', {
identity: initial.identity,
validSampleCount: 1,
});
assert.deepEqual(
captureRendererProcessRssSample(invalid, RENDERER_PID, [metric()]),
invalid
);
});
@@ -0,0 +1,267 @@
export type RendererProcessRssUnavailableReason =
| 'renderer-os-pid-invalid'
| 'renderer-process-identity-changed'
| 'renderer-process-metric-ambiguous'
| 'renderer-process-metric-creation-time-invalid'
| 'renderer-process-metric-missing-at-start'
| 'renderer-process-metric-missing-during-capture'
| 'renderer-process-metric-type-invalid'
| 'renderer-process-metric-working-set-invalid';
export interface RendererProcessIdentity {
readonly creationTime: number;
readonly pid: number;
}
export interface RendererProcessRssCapture {
readonly identity: RendererProcessIdentity | null;
readonly missingSampleCount: number;
readonly peakRssBytes: number | null;
readonly unavailableReason: RendererProcessRssUnavailableReason | null;
readonly validSampleCount: number;
}
export interface RendererProcessRssCaptureApi {
readonly create: (
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
) => RendererProcessRssCapture;
readonly sample: (
capture: RendererProcessRssCapture,
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
) => RendererProcessRssCapture;
}
export function createRendererProcessRssCaptureApi(): RendererProcessRssCaptureApi {
type JsonRecord = Record<string, unknown>;
const KIBIBYTE_BYTES = 1_024;
const MAX_WORKING_SET_KIB = Math.floor(
Number.MAX_SAFE_INTEGER / KIBIBYTE_BYTES
);
const helpers = {
readRecord(value: unknown): JsonRecord | null {
return typeof value === 'object' && value !== null
? (value as JsonRecord)
: null;
},
isValidOsPid(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) > 0;
},
findPidCandidates(
processMetrics: readonly unknown[],
pid: number
): JsonRecord[] {
return processMetrics
.map((metric) => helpers.readRecord(metric))
.filter(
(metric): metric is JsonRecord =>
metric !== null && metric['pid'] === pid
);
},
readMetric(metric: JsonRecord):
| {
readonly creationTime: number;
readonly rssBytes: number;
}
| RendererProcessRssUnavailableReason {
if (metric['type'] !== 'Tab' && metric['type'] !== 'Renderer') {
return 'renderer-process-metric-type-invalid';
}
const creationTime = metric['creationTime'];
if (
typeof creationTime !== 'number' ||
!Number.isFinite(creationTime) ||
creationTime < 0
) {
return 'renderer-process-metric-creation-time-invalid';
}
const memory = helpers.readRecord(metric['memory']);
const workingSetSize = memory?.['workingSetSize'];
if (
!Number.isSafeInteger(workingSetSize) ||
Number(workingSetSize) <= 0 ||
Number(workingSetSize) > MAX_WORKING_SET_KIB
) {
return 'renderer-process-metric-working-set-invalid';
}
return {
creationTime,
rssBytes: Number(workingSetSize) * KIBIBYTE_BYTES,
};
},
unavailableCapture(
unavailableReason: RendererProcessRssUnavailableReason,
options: {
readonly identity?: RendererProcessIdentity | null;
readonly missingSampleCount?: number;
readonly validSampleCount?: number;
} = {}
): RendererProcessRssCapture {
return Object.freeze({
identity: options.identity ?? null,
missingSampleCount: options.missingSampleCount ?? 0,
peakRssBytes: null,
unavailableReason,
validSampleCount: options.validSampleCount ?? 0,
});
},
invalidateCapture(
capture: RendererProcessRssCapture,
unavailableReason: RendererProcessRssUnavailableReason,
missingSampleCount = capture.missingSampleCount
): RendererProcessRssCapture {
return helpers.unavailableCapture(unavailableReason, {
identity: capture.identity,
missingSampleCount,
validSampleCount: capture.validSampleCount,
});
},
create(
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
if (!helpers.isValidOsPid(webContentsOsPid)) {
return helpers.unavailableCapture('renderer-os-pid-invalid');
}
const candidates = helpers.findPidCandidates(
processMetrics,
webContentsOsPid
);
if (candidates.length === 0) {
return helpers.unavailableCapture(
'renderer-process-metric-missing-at-start',
{
missingSampleCount: 1,
}
);
}
if (candidates.length !== 1) {
return helpers.unavailableCapture(
'renderer-process-metric-ambiguous'
);
}
const metric = helpers.readMetric(candidates[0] as JsonRecord);
if (typeof metric === 'string') {
return helpers.unavailableCapture(metric);
}
return Object.freeze({
identity: Object.freeze({
creationTime: metric.creationTime,
pid: webContentsOsPid,
}),
missingSampleCount: 0,
peakRssBytes: metric.rssBytes,
unavailableReason: null,
validSampleCount: 1,
});
},
sample(
capture: RendererProcessRssCapture,
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
if (capture.unavailableReason !== null) {
return capture;
}
if (!helpers.isValidOsPid(webContentsOsPid)) {
return helpers.invalidateCapture(
capture,
'renderer-os-pid-invalid'
);
}
const identity = capture.identity;
if (identity === null || webContentsOsPid !== identity.pid) {
return helpers.invalidateCapture(
capture,
'renderer-process-identity-changed'
);
}
const candidates = helpers.findPidCandidates(
processMetrics,
webContentsOsPid
);
if (candidates.length === 0) {
return helpers.invalidateCapture(
capture,
'renderer-process-metric-missing-during-capture',
capture.missingSampleCount + 1
);
}
if (candidates.length !== 1) {
return helpers.invalidateCapture(
capture,
'renderer-process-metric-ambiguous'
);
}
const metric = helpers.readMetric(candidates[0] as JsonRecord);
if (typeof metric === 'string') {
return helpers.invalidateCapture(capture, metric);
}
if (metric.creationTime !== identity.creationTime) {
return helpers.invalidateCapture(
capture,
'renderer-process-identity-changed'
);
}
return Object.freeze({
identity,
missingSampleCount: capture.missingSampleCount,
peakRssBytes: Math.max(
capture.peakRssBytes ?? metric.rssBytes,
metric.rssBytes
),
unavailableReason: null,
validSampleCount: capture.validSampleCount + 1,
});
},
};
return Object.freeze({
create: helpers.create,
sample: helpers.sample,
});
}
const rendererProcessRssCaptureApi = createRendererProcessRssCaptureApi();
export function createRendererProcessRssCapture(
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
return rendererProcessRssCaptureApi.create(
webContentsOsPid,
processMetrics
);
}
export function captureRendererProcessRssSample(
capture: RendererProcessRssCapture,
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
return rendererProcessRssCaptureApi.sample(
capture,
webContentsOsPid,
processMetrics
);
}
@@ -0,0 +1,153 @@
/* 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,
} from './m3u-refresh-cancellation-contract';
import { createCancellationBenchmarkSummary } from './m3u-refresh-cancellation-report';
import type { RendererProcessRssCapture } from './renderer-process-rss-capture';
function measuredIteration(
rss: RendererProcessRssCapture,
responsiveEvents: number,
unresponsiveEvents: number,
runId: string
): 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: 999_999_999,
rendererWindow: {
responsiveEvents,
rss,
unresponsiveEvents,
windowIdentity: {
browserWindowId: 7,
webContentsId: 11,
},
},
responsiveEvents: 100,
unresponsiveEvents: 100,
workers: [],
},
phases: {},
renderer: {
peakHeapUsedBytes: 0,
postGcHeapUsedBytes: null,
probe: {
frameGapsMs: [],
heartbeatDelaysMs: [],
longTasksMs: [],
},
},
runId,
} as unknown as CancellationIterationResult;
}
test('summary uses only exact target-window RSS and scoped responsiveness events', () => {
const summary = createCancellationBenchmarkSummary(
{} as CancellationBenchmarkManifest,
[
measuredIteration(
{
identity: {
creationTime: 1_721_234_567_890,
pid: 42,
},
missingSampleCount: 0,
peakRssBytes: 2_048,
unavailableReason: null,
validSampleCount: 3,
},
1,
2,
'run-01'
),
measuredIteration(
{
identity: {
creationTime: 1_721_234_567_890,
pid: 43,
},
missingSampleCount: 1,
peakRssBytes: null,
unavailableReason:
'renderer-process-metric-missing-during-capture',
validSampleCount: 2,
},
3,
4,
'run-02'
),
]
);
assert.deepEqual(summary.measured.rendererRssPeakBytes, {
count: 1,
max: 2_048,
mean: 2_048,
median: 2_048,
min: 2_048,
p95: 2_048,
p99: 2_048,
});
assert.equal(summary.measured.responsiveEvents, 4);
assert.equal(summary.measured.unresponsiveEvents, 6);
assert.deepEqual(
(
summary as unknown as {
readonly validity: {
readonly rendererRss: unknown;
};
}
).validity.rendererRss,
{
invalidMeasuredRuns: [
{
missingSampleCount: 1,
reason: 'renderer-process-metric-missing-during-capture',
runId: 'run-02',
validSampleCount: 2,
},
],
measuredRunCount: 2,
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 1,
}
);
});
test('benchmark preserves raw artifacts before rejecting incomplete renderer RSS', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const validityGuard = source.indexOf(
'summary.validity.rendererRss.validForBenchmark'
);
assert.ok(summaryWrite >= 0, 'summary artifact write must exist');
assert.ok(validityGuard >= 0, 'renderer RSS validity guard must exist');
assert.ok(
summaryWrite < validityGuard,
'raw summary must be durable before the renderer RSS guard fails'
);
assert.match(source, /Renderer RSS capture is invalid/);
});
@@ -0,0 +1,323 @@
/* 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 {
createRendererProcessRssCaptureApi,
type RendererProcessRssCapture,
type RendererProcessRssCaptureApi,
} from './renderer-process-rss-capture';
interface RendererWindowIdentity {
readonly browserWindowId: number;
readonly webContentsId: number;
}
interface RendererWindowRssSessionMetrics {
readonly responsiveEvents: number;
readonly rss: RendererProcessRssCapture;
readonly unresponsiveEvents: number;
readonly windowIdentity: RendererWindowIdentity;
}
interface RendererWindowRssSession {
detach(): void;
sample(): RendererWindowRssSessionMetrics;
snapshot(): RendererWindowRssSessionMetrics;
}
interface RendererWindowRssSessionFactory {
create(input: {
readonly browserWindowFromId: (
browserWindowId: number
) => FakeBrowserWindow | null;
readonly browserWindowId: number;
readonly getAppMetrics: () => readonly unknown[];
readonly rendererRssApi: RendererProcessRssCaptureApi;
readonly webContentsId: number;
}): RendererWindowRssSession;
}
interface RendererWindowRssSessionModule {
createRendererWindowRssSessionApi?: () => RendererWindowRssSessionFactory;
}
class FakeWebContents {
pidReads = 0;
throwOnPidRead = false;
constructor(
readonly id: number,
public pid: number
) {}
getOSProcessId(): number {
this.pidReads += 1;
if (this.throwOnPidRead) {
throw new Error('renderer unavailable');
}
return this.pid;
}
}
class FakeBrowserWindow extends EventEmitter {
constructor(
readonly id: number,
readonly webContents: FakeWebContents
) {
super();
}
}
const BROWSER_WINDOW_ID = 7;
const WEB_CONTENTS_ID = 70;
const RENDERER_PID = 700;
const CREATION_TIME = 1_721_234_567_890;
const sessionModulePromise = import(
new URL('./renderer-window-rss-session.ts', import.meta.url).href
)
.then((module) => module as RendererWindowRssSessionModule)
.catch(() => null);
function processMetric(
pid: number,
workingSetSize: number,
creationTime = CREATION_TIME
): unknown {
return {
creationTime,
memory: { workingSetSize },
pid,
type: 'Tab',
};
}
async function restoreSerializableFactory(): Promise<RendererWindowRssSessionFactory> {
const module = await sessionModulePromise;
assert.ok(module, 'renderer window RSS session module must exist');
const factory = module.createRendererWindowRssSessionApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => RendererWindowRssSessionFactory;
return restoredFactory();
}
test('scopes renderer RSS and window events to the exact BrowserWindow', async () => {
const api = await restoreSerializableFactory();
const targetWebContents = new FakeWebContents(
WEB_CONTENTS_ID,
RENDERER_PID
);
const targetWindow = new FakeBrowserWindow(
BROWSER_WINDOW_ID,
targetWebContents
);
const decoyWebContents = new FakeWebContents(71, 701);
const decoyWindow = new FakeBrowserWindow(8, decoyWebContents);
let targetExternalEvents = 0;
let decoyExternalEvents = 0;
targetWindow.on('unresponsive', () => {
targetExternalEvents += 1;
});
decoyWindow.on('unresponsive', () => {
decoyExternalEvents += 1;
});
let appMetricReads = 0;
const targetWorkingSets = [1_000, 1_500];
const session = api.create({
browserWindowFromId: (id) =>
id === BROWSER_WINDOW_ID ? targetWindow : decoyWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => {
const targetWorkingSet = targetWorkingSets[
Math.min(appMetricReads, 1)
] as number;
appMetricReads += 1;
return [
processMetric(701, 9_999_999, CREATION_TIME + 1),
processMetric(RENDERER_PID, targetWorkingSet),
];
},
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
decoyWindow.emit('unresponsive');
decoyWindow.emit('responsive');
targetWindow.emit('unresponsive');
targetWindow.emit('responsive');
const sampled = session.sample();
assert.deepEqual(sampled, {
rss: {
identity: {
creationTime: CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 1_536_000,
unavailableReason: null,
validSampleCount: 2,
},
responsiveEvents: 1,
unresponsiveEvents: 1,
windowIdentity: {
browserWindowId: BROWSER_WINDOW_ID,
webContentsId: WEB_CONTENTS_ID,
},
});
assert.equal(targetWebContents.pidReads, 2);
assert.equal(decoyWebContents.pidReads, 0);
assert.equal(appMetricReads, 2);
assert.equal(targetExternalEvents, 1);
assert.equal(decoyExternalEvents, 1);
assert.equal(targetWindow.listenerCount('unresponsive'), 2);
assert.equal(decoyWindow.listenerCount('unresponsive'), 1);
session.detach();
assert.equal(targetWindow.listenerCount('unresponsive'), 1);
assert.equal(targetWindow.listenerCount('responsive'), 0);
assert.equal(decoyWindow.listenerCount('unresponsive'), 1);
targetWindow.emit('unresponsive');
decoyWindow.emit('unresponsive');
assert.equal(session.snapshot().unresponsiveEvents, 1);
assert.equal(targetExternalEvents, 2);
assert.equal(decoyExternalEvents, 2);
});
test('rejects a missing or mismatched BrowserWindow before attaching listeners', async () => {
const api = await restoreSerializableFactory();
const targetWindow = new FakeBrowserWindow(
BROWSER_WINDOW_ID,
new FakeWebContents(WEB_CONTENTS_ID, RENDERER_PID)
);
let appMetricReads = 0;
const baseInput = {
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => {
appMetricReads += 1;
return [processMetric(RENDERER_PID, 1_000)];
},
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
};
assert.throws(
() =>
api.create({
...baseInput,
browserWindowFromId: () => null,
}),
/renderer-browser-window-missing/
);
assert.throws(
() =>
api.create({
...baseInput,
browserWindowFromId: () =>
new FakeBrowserWindow(
BROWSER_WINDOW_ID + 1,
targetWindow.webContents
),
}),
/renderer-browser-window-identity-mismatch/
);
assert.throws(
() =>
api.create({
...baseInput,
browserWindowFromId: () =>
new FakeBrowserWindow(
BROWSER_WINDOW_ID,
new FakeWebContents(WEB_CONTENTS_ID + 1, RENDERER_PID)
),
}),
/renderer-web-contents-identity-mismatch/
);
assert.equal(targetWindow.listenerCount('unresponsive'), 0);
assert.equal(targetWindow.listenerCount('responsive'), 0);
assert.equal(targetWindow.webContents.pidReads, 0);
assert.equal(appMetricReads, 0);
});
test('keeps PID and metric failures sticky while sampling the exact target every time', async () => {
const api = await restoreSerializableFactory();
const targetWebContents = new FakeWebContents(
WEB_CONTENTS_ID,
RENDERER_PID
);
const targetWindow = new FakeBrowserWindow(
BROWSER_WINDOW_ID,
targetWebContents
);
let appMetricReads = 0;
let metrics: readonly unknown[] = [processMetric(RENDERER_PID, 1_000)];
const session = api.create({
browserWindowFromId: () => targetWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => {
appMetricReads += 1;
return metrics;
},
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
targetWebContents.pid = RENDERER_PID + 1;
metrics = [processMetric(RENDERER_PID + 1, 5_000, CREATION_TIME + 1)];
const invalid = session.sample();
assert.deepEqual(invalid.rss, {
identity: {
creationTime: CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: null,
unavailableReason: 'renderer-process-identity-changed',
validSampleCount: 1,
});
targetWebContents.pid = RENDERER_PID;
metrics = [processMetric(RENDERER_PID, 10_000)];
assert.deepEqual(session.sample().rss, invalid.rss);
assert.equal(targetWebContents.pidReads, 3);
assert.equal(appMetricReads, 3);
const missingMetricSession = api.create({
browserWindowFromId: () => targetWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => [],
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
assert.deepEqual(missingMetricSession.snapshot().rss, {
identity: null,
missingSampleCount: 1,
peakRssBytes: null,
unavailableReason: 'renderer-process-metric-missing-at-start',
validSampleCount: 0,
});
targetWebContents.throwOnPidRead = true;
const invalidPidSession = api.create({
browserWindowFromId: () => targetWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => [processMetric(RENDERER_PID, 1_000)],
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
assert.equal(
invalidPidSession.snapshot().rss.unavailableReason,
'renderer-os-pid-invalid'
);
session.detach();
missingMetricSession.detach();
invalidPidSession.detach();
});
@@ -0,0 +1,174 @@
import type {
RendererProcessRssCapture,
RendererProcessRssCaptureApi,
} from './renderer-process-rss-capture';
export interface RendererWindowIdentity {
readonly browserWindowId: number;
readonly webContentsId: number;
}
export interface RendererWindowRssSessionMetrics {
readonly responsiveEvents: number;
readonly rss: RendererProcessRssCapture;
readonly unresponsiveEvents: number;
readonly windowIdentity: RendererWindowIdentity;
}
export interface RendererWindowWebContents {
readonly id: number;
getOSProcessId(): number;
}
export interface RendererWindow {
readonly id: number;
readonly webContents: RendererWindowWebContents;
off(event: 'responsive' | 'unresponsive', listener: () => void): unknown;
on(event: 'responsive' | 'unresponsive', listener: () => void): unknown;
}
export interface RendererWindowRssSession {
detach(): void;
sample(): RendererWindowRssSessionMetrics;
snapshot(): RendererWindowRssSessionMetrics;
}
export interface RendererWindowRssSessionCreateInput {
readonly browserWindowFromId: (
browserWindowId: number
) => RendererWindow | null;
readonly browserWindowId: number;
readonly getAppMetrics: () => readonly unknown[];
readonly rendererRssApi: RendererProcessRssCaptureApi;
readonly webContentsId: number;
}
export interface RendererWindowRssSessionApi {
create(
input: RendererWindowRssSessionCreateInput
): RendererWindowRssSession;
}
export function createRendererWindowRssSessionApi(): RendererWindowRssSessionApi {
const helpers = {
isValidIdentityId(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) > 0;
},
create(
input: RendererWindowRssSessionCreateInput
): RendererWindowRssSession {
if (!helpers.isValidIdentityId(input.browserWindowId)) {
throw new Error('renderer-browser-window-id-invalid');
}
if (!helpers.isValidIdentityId(input.webContentsId)) {
throw new Error('renderer-web-contents-id-invalid');
}
let browserWindow: RendererWindow | null = null;
try {
browserWindow = input.browserWindowFromId(
input.browserWindowId
);
} catch {
throw new Error('renderer-browser-window-missing');
}
if (browserWindow === null) {
throw new Error('renderer-browser-window-missing');
}
if (browserWindow.id !== input.browserWindowId) {
throw new Error('renderer-browser-window-identity-mismatch');
}
const webContents = browserWindow.webContents;
if (
webContents === null ||
typeof webContents !== 'object' ||
webContents.id !== input.webContentsId
) {
throw new Error('renderer-web-contents-identity-mismatch');
}
const windowIdentity = Object.freeze({
browserWindowId: input.browserWindowId,
webContentsId: input.webContentsId,
});
let rendererRss = input.rendererRssApi.create(
helpers.readOsProcessId(webContents),
helpers.readAppMetrics(input.getAppMetrics)
);
let responsiveEvents = 0;
let unresponsiveEvents = 0;
let detached = false;
const callbacks = {
onResponsive(): void {
if (!detached) {
responsiveEvents += 1;
}
},
onUnresponsive(): void {
if (!detached) {
unresponsiveEvents += 1;
}
},
snapshot(): RendererWindowRssSessionMetrics {
return Object.freeze({
responsiveEvents,
rss: rendererRss,
unresponsiveEvents,
windowIdentity,
});
},
};
browserWindow.on('unresponsive', callbacks.onUnresponsive);
try {
browserWindow.on('responsive', callbacks.onResponsive);
} catch (error) {
browserWindow.off('unresponsive', callbacks.onUnresponsive);
throw error;
}
return Object.freeze({
detach(): void {
if (detached) {
return;
}
detached = true;
browserWindow.off('unresponsive', callbacks.onUnresponsive);
browserWindow.off('responsive', callbacks.onResponsive);
},
sample(): RendererWindowRssSessionMetrics {
if (!detached) {
rendererRss = input.rendererRssApi.sample(
rendererRss,
helpers.readOsProcessId(webContents),
helpers.readAppMetrics(input.getAppMetrics)
);
}
return callbacks.snapshot();
},
snapshot: callbacks.snapshot,
});
},
readAppMetrics(
getAppMetrics: () => readonly unknown[]
): readonly unknown[] {
try {
const metrics = getAppMetrics();
return Array.isArray(metrics) ? metrics : [];
} catch {
return [];
}
},
readOsProcessId(webContents: RendererWindowWebContents): unknown {
try {
return webContents.getOSProcessId();
} catch {
return null;
}
},
};
return Object.freeze({ create: helpers.create });
}
@@ -12,6 +12,7 @@ import {
type WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
type MainCaptureGenerationTransport,
normalizeWorkerRequestPerformanceOutcome,
selectMainCaptureGeneration,
} from './worker-request-performance';
@@ -130,9 +131,24 @@ function measuredIteration(
postGcHeapUsedBytes: null,
postGcRssBytes: null,
},
rendererPeakRssBytes: 0,
responsiveEvents: 0,
unresponsiveEvents: 0,
rendererWindow: {
responsiveEvents: 0,
rss: {
identity: {
creationTime: 1_721_234_567_890,
pid: 42,
},
missingSampleCount: 0,
peakRssBytes: 0,
unavailableReason: null,
validSampleCount: 1,
},
unresponsiveEvents: 0,
windowIdentity: {
browserWindowId: 7,
webContentsId: 11,
},
},
workers,
} as CancellationIterationResult['main'],
phases: {} as CancellationIterationResult['phases'],
@@ -202,7 +218,6 @@ test('main capture retains raw request identity, timestamps, metrics, and reason
);
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/);
@@ -214,6 +229,26 @@ test('main capture retains raw request identity, timestamps, metrics, and reason
);
});
test('benchmark resolves one exact BrowserWindow and main capture never scans all windows', () => {
const benchmarkSource = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const mainCaptureSource = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
assert.match(
benchmarkSource,
/electronApp\.browserWindow\(\s*app\.mainWindow\s*\)/
);
assert.match(benchmarkSource, /rendererWindowIdentity/);
assert.doesNotMatch(mainCaptureSource, /BrowserWindow\.getAllWindows/);
assert.doesNotMatch(mainCaptureSource, /type\.includes\('renderer'\)/);
assert.doesNotMatch(mainCaptureSource, /type\.includes\('tab'\)/);
});
test('capture-generation selection excludes a pre-start seed worker', () => {
const seed = playlistWorker(
requestPerformance('seed-request', 999, 999),
@@ -259,6 +294,53 @@ test('capture-generation selection excludes a pre-start seed worker', () => {
assert.equal(selected.workers[0]?.terminatedEpochMs, 2_100);
});
test('fails closed for incoherent worker post-GC metrics at the Electron boundary', () => {
const invalidOutcomes = [
{
postGcHeapUnavailableReason: 'gc-unavailable',
postGcHeapUsedBytes: 100,
},
{
postGcHeapUnavailableReason: null,
postGcHeapUsedBytes: null,
},
{
postGcHeapUnavailableReason: null,
postGcHeapUsedBytes: -1,
},
{
postGcHeapUnavailableReason: 'unknown-reason',
postGcHeapUsedBytes: null,
},
] as const;
for (const outcome of invalidOutcomes) {
const selected = selectMainCaptureGeneration({
captureGeneration: 3,
metrics: measuredIteration([]).main,
workers: [
{
captureGeneration: 3,
metrics: {
...databaseWorker([]),
...outcome,
},
requests: [],
},
],
} as unknown as MainCaptureGenerationTransport);
const worker = selected.workers[0] as WorkerCaptureMetrics & {
readonly postGcHeapUnavailableReason: string | null;
};
assert.equal(worker.postGcHeapUsedBytes, null);
assert.equal(
worker.postGcHeapUnavailableReason,
'post-gc-capture-invalid'
);
}
});
test('raw outcomes retain missing and malformed captures while summaries exclude their null metrics', () => {
const valid = normalizeWorkerRequestPerformanceOutcome(
requestTransport(requestPerformance('request-1', 5, 100))
@@ -2,8 +2,10 @@ import type {
EventLoopDelayMetrics,
MainCaptureMetrics,
WorkerCaptureMetrics,
WorkerPostGcHeapCapture,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import { WORKER_POST_GC_HEAP_UNAVAILABLE_REASON } from './m3u-refresh-cancellation-contract';
export interface WorkerRequestPerformanceOutcomeTransport {
readonly operation: string | null;
@@ -62,6 +64,9 @@ const THREAD_CPU_REASONS = new Set([
'thread-cpu-usage-invalid',
'thread-cpu-usage-unavailable',
]);
const WORKER_POST_GC_HEAP_UNAVAILABLE_REASONS = new Set<string>(
Object.values(WORKER_POST_GC_HEAP_UNAVAILABLE_REASON)
);
function isRecord(input: unknown): input is Record<string, unknown> {
return typeof input === 'object' && input !== null && !Array.isArray(input);
@@ -84,6 +89,38 @@ function isNullableReason(
return value === null || (typeof value === 'string' && allowed.has(value));
}
function normalizeWorkerPostGcHeapCapture(
input: Record<string, unknown>
): WorkerPostGcHeapCapture {
const postGcHeapUnavailableReason = input['postGcHeapUnavailableReason'];
const postGcHeapUsedBytes = input['postGcHeapUsedBytes'];
if (
Number.isSafeInteger(postGcHeapUsedBytes) &&
Number(postGcHeapUsedBytes) >= 0 &&
postGcHeapUnavailableReason === null
) {
return {
postGcHeapUnavailableReason: null,
postGcHeapUsedBytes: Number(postGcHeapUsedBytes),
};
}
if (
postGcHeapUsedBytes === null &&
typeof postGcHeapUnavailableReason === 'string' &&
WORKER_POST_GC_HEAP_UNAVAILABLE_REASONS.has(postGcHeapUnavailableReason)
) {
return {
postGcHeapUnavailableReason,
postGcHeapUsedBytes: null,
} as WorkerPostGcHeapCapture;
}
return {
postGcHeapUnavailableReason:
WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.INVALID_CAPTURE,
postGcHeapUsedBytes: null,
};
}
function parseEventLoopDelay(
input: unknown
): EventLoopDelayMetrics | null | undefined {
@@ -280,8 +317,12 @@ export function selectMainCaptureGeneration(
);
const onlyRequest =
requests.length === 1 ? (requests[0] ?? null) : null;
const postGcHeap = normalizeWorkerPostGcHeapCapture(
worker.metrics as unknown as Record<string, unknown>
);
return {
...worker.metrics,
...postGcHeap,
eventLoopDelay: onlyRequest?.eventLoopDelay ?? null,
eventLoopDelayUnavailableReason: onlyRequest
? (onlyRequest.eventLoopDelayUnavailableReason ??
@@ -0,0 +1,102 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
interface WorkerTerminationGenerationApi {
isCurrent(input: {
readonly capturedGeneration: number | null;
readonly currentGeneration: number;
readonly recordGeneration: number | null;
}): boolean;
}
interface WorkerTerminationGenerationModule {
createWorkerTerminationGenerationApi?: () => WorkerTerminationGenerationApi;
}
const modulePromise = import(
new URL('./worker-termination-generation.ts', import.meta.url).href
)
.then((module) => module as WorkerTerminationGenerationModule)
.catch(() => null);
test('rejects a delayed termination completion after capture rollover or record reuse', async () => {
const module = await modulePromise;
assert.ok(module, 'worker termination generation guard must exist');
const api = module.createWorkerTerminationGenerationApi?.();
assert.ok(api);
assert.equal(
api.isCurrent({
capturedGeneration: 4,
currentGeneration: 4,
recordGeneration: 4,
}),
true
);
assert.equal(
api.isCurrent({
capturedGeneration: 4,
currentGeneration: 5,
recordGeneration: 4,
}),
false,
'a late completion must not write into the next global capture generation'
);
assert.equal(
api.isCurrent({
capturedGeneration: 4,
currentGeneration: 5,
recordGeneration: 5,
}),
false,
'a reused record must not be finalized by the prior termination promise'
);
});
test('fails closed for missing or invalid generation identities', async () => {
const module = await modulePromise;
assert.ok(module);
const api = module.createWorkerTerminationGenerationApi?.();
assert.ok(api);
for (const capturedGeneration of [null, 0, -1, 1.5, Number.NaN]) {
assert.equal(
api.isCurrent({
capturedGeneration,
currentGeneration: 1,
recordGeneration: 1,
}),
false
);
}
});
test('the Electron termination callback applies the generation guard before mutating capture state', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const terminateStart = source.indexOf(
'WorkerClass.prototype.terminate = function'
);
const terminateEnd = source.indexOf('const inspectorPost', terminateStart);
const terminateBlock = source.slice(terminateStart, terminateEnd);
const terminationGeneration = terminateBlock.indexOf(
'const terminationGeneration = record.captureGeneration'
);
const generationGuard = terminateBlock.indexOf(
'workerTerminationGenerationApi.isCurrent'
);
const terminationMutation = terminateBlock.indexOf(
'record.terminatedEpochMs = nowEpochMs()'
);
assert.ok(
terminationGeneration >= 0 &&
terminationGeneration < generationGuard &&
generationGuard < terminationMutation,
'late termination completion must be generation-gated before mutating the record or timeline'
);
});
@@ -0,0 +1,24 @@
export interface WorkerTerminationGenerationInput {
readonly capturedGeneration: number | null;
readonly currentGeneration: number;
readonly recordGeneration: number | null;
}
export interface WorkerTerminationGenerationApi {
isCurrent(input: WorkerTerminationGenerationInput): boolean;
}
export function createWorkerTerminationGenerationApi(): WorkerTerminationGenerationApi {
const isCaptureGeneration = (value: number | null): value is number =>
Number.isSafeInteger(value) && Number(value) > 0;
return Object.freeze({
isCurrent(input: WorkerTerminationGenerationInput): boolean {
return (
isCaptureGeneration(input.capturedGeneration) &&
input.capturedGeneration === input.currentGeneration &&
input.capturedGeneration === input.recordGeneration
);
},
});
}
@@ -0,0 +1,303 @@
import { once } from 'node:events';
import { getHeapStatistics } from 'node:v8';
import { MessageChannel, type MessagePort } from 'node:worker_threads';
import {
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON,
handleDatabaseWorkerPostGcHeapRequest,
isDatabaseWorkerPostGcHeapRequest,
type DatabaseWorkerPostGcHeapRequest,
type DatabaseWorkerPostGcHeapResult,
} from './database-worker-post-gc-heap';
jest.mock('node:v8', () => ({
getHeapStatistics: jest.fn(),
}));
const PROFILING_ENV = 'IPTVNATOR_PERF_WORKER_PROFILING';
const mockedGetHeapStatistics = getHeapStatistics as jest.MockedFunction<
typeof getHeapStatistics
>;
type FakeResponsePort = {
close: jest.Mock<void, []>;
messages: DatabaseWorkerPostGcHeapResult[];
port: MessagePort;
postMessage: jest.Mock<void, [DatabaseWorkerPostGcHeapResult]>;
};
function createFakeResponsePort(): FakeResponsePort {
const messages: DatabaseWorkerPostGcHeapResult[] = [];
const close = jest.fn<void, []>();
const postMessage = jest.fn<void, [DatabaseWorkerPostGcHeapResult]>(
(message) => {
messages.push(message);
}
);
return {
close,
messages,
port: {
close,
postMessage,
} as unknown as MessagePort,
postMessage,
};
}
function handleWithFakePort(isIdle: boolean): {
gc: jest.Mock<void, []> | null;
responsePort: FakeResponsePort;
} {
const responsePort = createFakeResponsePort();
const gcCandidate = Reflect.get(globalThis, 'gc');
const request: DatabaseWorkerPostGcHeapRequest = {
type: 'performance:collect-post-gc-heap',
responsePort: responsePort.port,
};
handleDatabaseWorkerPostGcHeapRequest(request, isIdle);
return {
gc:
typeof gcCandidate === 'function'
? (gcCandidate as unknown as jest.Mock<void, []>)
: null,
responsePort,
};
}
describe('database worker post-GC heap capture', () => {
const originalProfilingValue = process.env[PROFILING_ENV];
const originalGc = Reflect.get(globalThis, 'gc');
beforeEach(() => {
process.env[PROFILING_ENV] = '1';
Reflect.set(globalThis, 'gc', jest.fn<void, []>());
mockedGetHeapStatistics.mockReset();
mockedGetHeapStatistics.mockReturnValue({
used_heap_size: 42_000,
} as ReturnType<typeof getHeapStatistics>);
});
afterAll(() => {
if (originalProfilingValue === undefined) {
delete process.env[PROFILING_ENV];
} else {
process.env[PROFILING_ENV] = originalProfilingValue;
}
if (originalGc === undefined) {
Reflect.deleteProperty(globalThis, 'gc');
} else {
Reflect.set(globalThis, 'gc', originalGc);
}
});
it('recognizes only the exact request with a real MessagePort', () => {
const { port1, port2 } = new MessageChannel();
try {
expect(
isDatabaseWorkerPostGcHeapRequest({
type: 'performance:collect-post-gc-heap',
responsePort: port2,
})
).toBe(true);
expect(isDatabaseWorkerPostGcHeapRequest(null)).toBe(false);
expect(
isDatabaseWorkerPostGcHeapRequest({
type: 'performance:collect-post-gc-heap',
})
).toBe(false);
expect(
isDatabaseWorkerPostGcHeapRequest({
type: 'performance:collect-post-gc-heap',
responsePort: {
close: jest.fn(),
postMessage: jest.fn(),
},
})
).toBe(false);
expect(
isDatabaseWorkerPostGcHeapRequest({
type: 'request',
responsePort: port2,
})
).toBe(false);
} finally {
port1.close();
port2.close();
}
});
it('forces GC before reading and returning the worker V8 heap', async () => {
const lifecycle: string[] = [];
const { port1, port2 } = new MessageChannel();
Reflect.set(
globalThis,
'gc',
jest.fn(() => lifecycle.push('gc'))
);
mockedGetHeapStatistics.mockImplementation(() => {
lifecycle.push('heap');
return {
used_heap_size: 73_728,
} as ReturnType<typeof getHeapStatistics>;
});
const responsePromise = once(port1, 'message') as Promise<
[DatabaseWorkerPostGcHeapResult]
>;
const peerClosedPromise = once(port1, 'close');
handleDatabaseWorkerPostGcHeapRequest(
{
type: 'performance:collect-post-gc-heap',
responsePort: port2,
},
true
);
await expect(responsePromise).resolves.toEqual([
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: 73_728,
unavailableReason: null,
},
]);
await peerClosedPromise;
expect(lifecycle).toEqual(['gc', 'heap']);
});
it('fails closed without profiling opt-in', () => {
delete process.env[PROFILING_ENV];
const { gc, responsePort } = handleWithFakePort(true);
expect(gc).not.toHaveBeenCalled();
expect(mockedGetHeapStatistics).not.toHaveBeenCalled();
expect(responsePort.messages).toEqual([
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason:
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.PROFILING_DISABLED,
},
]);
expect(responsePort.postMessage).toHaveBeenCalledTimes(1);
expect(responsePort.close).toHaveBeenCalledTimes(1);
});
it('does not perturb an active database operation', () => {
const { gc, responsePort } = handleWithFakePort(false);
expect(gc).not.toHaveBeenCalled();
expect(mockedGetHeapStatistics).not.toHaveBeenCalled();
expect(responsePort.messages).toEqual([
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason:
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.WORKER_BUSY,
},
]);
expect(responsePort.postMessage).toHaveBeenCalledTimes(1);
expect(responsePort.close).toHaveBeenCalledTimes(1);
});
it('reports an unavailable exposed-GC runtime without reading heap statistics', () => {
Reflect.deleteProperty(globalThis, 'gc');
const { responsePort } = handleWithFakePort(true);
expect(mockedGetHeapStatistics).not.toHaveBeenCalled();
expect(responsePort.messages).toEqual([
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason:
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.GC_UNAVAILABLE,
},
]);
expect(responsePort.postMessage).toHaveBeenCalledTimes(1);
expect(responsePort.close).toHaveBeenCalledTimes(1);
});
it.each([
['negative', -1],
['fractional', 1.5],
['NaN', Number.NaN],
['infinite', Number.POSITIVE_INFINITY],
['unsafe', Number.MAX_SAFE_INTEGER + 1],
])('rejects a %s heap measurement', (_label, usedHeapSize) => {
mockedGetHeapStatistics.mockReturnValue({
used_heap_size: usedHeapSize,
} as ReturnType<typeof getHeapStatistics>);
const { gc, responsePort } = handleWithFakePort(true);
expect(gc).toHaveBeenCalledTimes(1);
expect(mockedGetHeapStatistics).toHaveBeenCalledTimes(1);
expect(responsePort.messages).toEqual([
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason:
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.CAPTURE_FAILED,
},
]);
expect(responsePort.postMessage).toHaveBeenCalledTimes(1);
expect(responsePort.close).toHaveBeenCalledTimes(1);
});
it.each(['gc', 'heap'] as const)(
'reports capture failure when %s collection throws',
(failurePoint) => {
if (failurePoint === 'gc') {
Reflect.set(
globalThis,
'gc',
jest.fn(() => {
throw new Error('gc failed');
})
);
} else {
mockedGetHeapStatistics.mockImplementation(() => {
throw new Error('heap failed');
});
}
const { responsePort } = handleWithFakePort(true);
expect(responsePort.messages).toEqual([
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason:
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.CAPTURE_FAILED,
},
]);
expect(responsePort.postMessage).toHaveBeenCalledTimes(1);
expect(responsePort.close).toHaveBeenCalledTimes(1);
}
);
it('still closes the one-shot port when posting the result fails', () => {
const responsePort = createFakeResponsePort();
responsePort.postMessage.mockImplementation(() => {
throw new Error('port closed');
});
expect(() =>
handleDatabaseWorkerPostGcHeapRequest(
{
type: 'performance:collect-post-gc-heap',
responsePort: responsePort.port,
},
true
)
).not.toThrow();
expect(responsePort.postMessage).toHaveBeenCalledTimes(1);
expect(responsePort.close).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,124 @@
import { getHeapStatistics } from 'node:v8';
import { MessagePort } from 'node:worker_threads';
import { WORKER_PROFILING_ENV } from './worker-performance-capture.runtime';
export const DATABASE_WORKER_POST_GC_HEAP_REQUEST_TYPE =
'performance:collect-post-gc-heap' as const;
export const DATABASE_WORKER_POST_GC_HEAP_RESULT_TYPE =
'performance:post-gc-heap-result' as const;
export const DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON = {
CAPTURE_FAILED: 'capture-failed',
GC_UNAVAILABLE: 'gc-unavailable',
PROFILING_DISABLED: 'profiling-disabled',
WORKER_BUSY: 'worker-busy',
} as const;
export type DatabaseWorkerPostGcHeapUnavailableReason =
(typeof DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON)[keyof typeof DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON];
export interface DatabaseWorkerPostGcHeapRequest {
readonly type: typeof DATABASE_WORKER_POST_GC_HEAP_REQUEST_TYPE;
readonly responsePort: MessagePort;
}
export type DatabaseWorkerPostGcHeapResult =
| {
readonly type: typeof DATABASE_WORKER_POST_GC_HEAP_RESULT_TYPE;
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly type: typeof DATABASE_WORKER_POST_GC_HEAP_RESULT_TYPE;
readonly postGcHeapUsedBytes: null;
readonly unavailableReason: DatabaseWorkerPostGcHeapUnavailableReason;
};
export function isDatabaseWorkerPostGcHeapRequest(
message: unknown
): message is DatabaseWorkerPostGcHeapRequest {
if (
typeof message !== 'object' ||
message === null ||
Array.isArray(message)
) {
return false;
}
const candidate = message as Record<string, unknown>;
return (
candidate['type'] === DATABASE_WORKER_POST_GC_HEAP_REQUEST_TYPE &&
candidate['responsePort'] instanceof MessagePort
);
}
function unavailableResult(
unavailableReason: DatabaseWorkerPostGcHeapUnavailableReason
): DatabaseWorkerPostGcHeapResult {
return {
type: DATABASE_WORKER_POST_GC_HEAP_RESULT_TYPE,
postGcHeapUsedBytes: null,
unavailableReason,
};
}
function postResult(
responsePort: MessagePort,
result: DatabaseWorkerPostGcHeapResult
): void {
try {
responsePort.postMessage(result);
} catch {
// Development-only profiling transport must not affect worker behavior.
} finally {
try {
responsePort.close();
} catch {
// The one-shot port may already be closed by its peer.
}
}
}
export function handleDatabaseWorkerPostGcHeapRequest(
message: DatabaseWorkerPostGcHeapRequest,
isIdle: boolean
): void {
let result: DatabaseWorkerPostGcHeapResult;
if (process.env[WORKER_PROFILING_ENV] !== '1') {
result = unavailableResult(
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.PROFILING_DISABLED
);
} else if (!isIdle) {
result = unavailableResult(
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.WORKER_BUSY
);
} else if (typeof globalThis.gc !== 'function') {
result = unavailableResult(
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.GC_UNAVAILABLE
);
} else {
try {
globalThis.gc();
const postGcHeapUsedBytes = getHeapStatistics().used_heap_size;
result =
Number.isSafeInteger(postGcHeapUsedBytes) &&
postGcHeapUsedBytes >= 0
? {
type: DATABASE_WORKER_POST_GC_HEAP_RESULT_TYPE,
postGcHeapUsedBytes,
unavailableReason: null,
}
: unavailableResult(
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.CAPTURE_FAILED
);
} catch {
result = unavailableResult(
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.CAPTURE_FAILED
);
}
}
postResult(message.responsePort, result);
}
@@ -96,6 +96,10 @@ import {
startWorkerPerformanceCapture,
type WorkerPerformanceCapture,
} from './worker-performance-capture';
import {
handleDatabaseWorkerPostGcHeapRequest,
isDatabaseWorkerPostGcHeapRequest,
} from './database-worker-post-gc-heap';
const loggerLabel = '[DB Worker]';
const batchDelayMs = Number.parseInt(
@@ -989,6 +993,14 @@ async function executeRequest(
}
parentPort.on('message', async (message: DbWorkerIncomingMessage) => {
if (isDatabaseWorkerPostGcHeapRequest(message)) {
handleDatabaseWorkerPostGcHeapRequest(
message,
activePerformanceCaptures.size === 0
);
return;
}
if (message.type === 'cancel') {
const activeOperation = activeOperations.get(message.operationId);
if (activeOperation) {
@@ -311,3 +311,51 @@ describe('worker cancellation while performance capture arms', () => {
);
});
});
describe('database worker performance control messages', () => {
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it('handles a post-GC probe without dispatching a database request', async () => {
const armGate = createArmGate();
armGate.release();
const port = createParentPortHarness<unknown, DbWorkerMessage>();
const getWorkerDatabase = jest.fn().mockResolvedValue({});
const handlePostGcHeapRequest = jest.fn();
const responsePort = { marker: 'response-port' };
mockPerformanceCapture(armGate);
jest.doMock('worker_threads', () => ({
parentPort: port.parentPort,
}));
jest.doMock('./database.worker-connection', () => ({
closeWorkerDatabase: jest.fn(),
getWorkerDatabase,
}));
jest.doMock('./database-worker-post-gc-heap', () => ({
handleDatabaseWorkerPostGcHeapRequest: handlePostGcHeapRequest,
isDatabaseWorkerPostGcHeapRequest: jest.fn(
(message: unknown) =>
typeof message === 'object' &&
message !== null &&
(message as Record<string, unknown>)['type'] ===
'performance:collect-post-gc-heap'
),
}));
await import('./database.worker');
port.postMessage.mockClear();
const handleMessage = port.getMessageHandler();
const message = {
type: 'performance:collect-post-gc-heap',
responsePort,
};
await handleMessage(message);
expect(handlePostGcHeapRequest).toHaveBeenCalledWith(message, true);
expect(getWorkerDatabase).not.toHaveBeenCalled();
expect(port.postMessage).not.toHaveBeenCalled();
});
});
+65 -5
View File
@@ -50,7 +50,7 @@ All four parse call sites (Electron `playlist-source.ts` import, `playlist-refre
- **`radio` attribute** — `item.radio` (string, `'true'` triggers the radio player, EPG suppression, and external-player gating app-wide). Upstream does not have this field; it must survive every upstream sync.
- **Pipe stripping** — `item.url` is cut at the first `|`; `|User-Agent=` / `|Referer=` params still land in `item.http`. Upstream 0.15.0 stopped stripping, but iptvnator consumes `item.url` verbatim in hls.js/mpv/vlc, catch-up URL building, and url-keyed favorites.
- **`#KODIPROP` lines before `#EXTINF` are preserved** (since `v0.15.2-iptvnator.2`) — Kodi property lines apply to the *next* list entry, so ones placed above the `#EXTINF` are buffered and attached to that item's `raw` in file order (case-insensitive prefix); other stray `#` lines outside an open item are still dropped. The DASH + ClearKey feature extracts `inputstream.adaptive.license_*` config from `item.raw`, so this delta must survive every upstream sync.
- **`#KODIPROP` lines before `#EXTINF` are preserved** (since `v0.15.2-iptvnator.2`) — Kodi property lines apply to the _next_ list entry, so ones placed above the `#EXTINF` are buffered and attached to that item's `raw` in file order (case-insensitive prefix); other stray `#` lines outside an open item are still dropped. The DASH + ClearKey feature extracts `inputstream.adaptive.license_*` config from `item.raw`, so this delta must survive every upstream sync.
There is intentionally **no URL validation** (upstream removed it in 0.15.0): any non-empty non-`#` line after `#EXTINF` becomes the item URL. This is what fixes issue #1189 (Pluto TV JWT URLs longer than validator's 2084-char IE-era limit used to be rejected, and the stalled item index collapsed the whole playlist into one channel). `#` comment lines and unknown directives are appended to `item.raw` and never treated as URLs.
@@ -123,7 +123,16 @@ The target reserves and verifies CDP port 9222, freezes renderer long-task,
frame-gap, and heartbeat probes before forced post-GC heap collection, and
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
profiling. Renderer RSS is scoped to the Playwright page's exact
`BrowserWindow` and `webContents`: every sample matches `getOSProcessId()` to
one exact `app.getAppMetrics()` PID and creation time, while responsive and
unresponsive events come only from that window. Identity changes, missing or
ambiguous process metrics, and invalid working-set values fail closed with a
raw reason and nullable RSS; summaries exclude unavailable RSS instead of
reporting zero. Formal runs also list every invalid measured capture under
`summary.validity.rendererRss` and fail after persisting the raw summary unless
all measured iterations contribute one valid exact-window RSS value. 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
@@ -131,8 +140,59 @@ 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
Database-worker post-GC heap is not inferred from a heap snapshot. The harness
selects exactly one current-generation database worker independently of its
sampling timer, waits for any in-flight sample plus one final sample, stops the
worker CPU profile, sends an explicit-GC one-shot probe over a transferred
`MessagePort`, and takes an optional diagnostic snapshot only afterward.
Capture stop first closes a synchronous request cutoff and stops the main CPU
profile, so worker profile writes and heap snapshots cannot appear as
application stacks in `main.cpuprofile`. A database request observed after that
cutoff cannot restart worker sampling or profiling; it records
`database-worker-activity-after-cutoff` and invalidates the post-GC value.
Worker artifacts include a stable isolate ordinal in their filename so an
invalid multiple-worker capture cannot overwrite another isolate's profile.
The raw heap and unavailability reason form a strict XOR; playlist workers
terminated during cancellation report `worker-force-terminated-before-gc`
instead of a snapshot-derived heap. Warm-up and measured cancellation invoke
the real worker termination before best-effort finalization so profiling cannot
delay headline acknowledgement metrics. The separate diagnostic iteration
stops its CPU profile before termination; its timings are excluded from
headline distributions. That pre-termination drain is bounded; timeout
invalidates the profile and worker termination still proceeds. Late
termination completions are generation-gated so they cannot mutate a reused
record or append timeline events after an atomic capture rollover. The
benchmark fails closed unless the diagnostic iteration observed cancellation,
its profile is inside the iteration directory and parses with non-empty nodes
and samples, and the dying worker has no heap snapshot.
Headline worker memory distributions include every matching isolate and
exclude only nullable unavailable values. This aggregation is not used as a
validity shortcut. Runs that reach database persistence must independently
contain exactly one database worker with a coherent numeric post-GC heap.
Parsing cancellation happens before the renderer dispatches persistence, so a
run with no database request is explicitly `N/A:
operation-cancelled-before-database-phase`, not a missing capture. Any database
activity in a run that observed the cancellation effect is unrelated
contamination and invalidates the run. Duplicate, busy, late, timed-out, or
malformed captures are listed under
`summary.validity.databaseWorkerPostGc` and invalidate the benchmark. The
benchmark writes `manifest.json` and `summary.json` first, then fails the formal
run so the raw evidence remains available without supporting a before/after
claim. A formal cancellation run also requires the cancellation effect in every
measured iteration. Database-worker values are comparable only when every
measured iteration reaches that phase and has one valid capture.
Seed setup runs inside its own capture generation. The harness requires an
observed seed database request, a completed playlist upsert, and no pending
request, then persists that generation as `seed-main-capture.json`. Main
capture finalization validates one idle database worker with a coherent
explicit-GC result and atomically rolls the clean cutoff into the measured
generation before yielding. A request before rollover remains late and rejects
the transition; a request after rollover belongs to the measured generation.
This prevents seed writes or an unobserved stop/start gap from crossing into
measured main RSS, worker peaks, or profiles; generation selection also
excludes the seed worker record. 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
@@ -811,7 +871,7 @@ player in settings.
1. The playlist parser fork does not interpret `#KODIPROP:` lines, but
preserves them in `item.raw` for **both** layouts: unknown lines between
`#EXTINF` and the stream URL are kept as before, and since parser pin
`v0.15.2-iptvnator.2` `#KODIPROP` lines placed *before* the `#EXTINF` are
`v0.15.2-iptvnator.2` `#KODIPROP` lines placed _before_ the `#EXTINF` are
buffered and attached to the **next** entry's `raw` in file order (Kodi
semantics, case-insensitive prefix). Other stray `#` lines outside an open
item are still dropped, matching upstream.
+36 -1
View File
@@ -147,6 +147,40 @@ profiling queue. If captures overlap, every overlapping response carries
CPU, ELU, and event-loop-delay values are `null`. This avoids assigning shared
worker activity to one request while preserving normal worker concurrency.
### Opt-in post-GC heap capture
The same `IPTVNATOR_PERF_WORKER_PROFILING=1` opt-in enables a development/test
one-shot `performance:collect-post-gc-heap` request. The benchmark transfers a
dedicated `MessagePort`; the database worker accepts the request only while no
request performance capture is active, calls exposed `globalThis.gc()`, reads
its own V8 isolate through `v8.getHeapStatistics()`, posts one result, and
closes the port. Production launches do not expose GC or send this request.
The response is a strict XOR: either a non-negative
`postGcHeapUsedBytes` with a `null` reason, or a `null` heap with a fixed
unavailability reason. Disabled profiling, a busy worker, unavailable GC, and
capture failure all fail closed without changing the database operation or
terminating the worker. The performance launcher supplies
`--js-flags=--expose-gc` to Electron itself; worker `execArgv` remains
untouched.
The main-process benchmark selects exactly one current-generation database
worker, waits for its final sampling call, stops its CPU profile, performs the
explicit-GC probe, and only then takes an optional diagnostic heap snapshot.
Profile or snapshot failure cannot overwrite an already captured post-GC
value. Capture stop closes a synchronous cutoff before awaiting profile or
snapshot work. A database request after that cutoff cannot restart sampling;
it records `database-worker-activity-after-cutoff` and invalidates the result.
Missing, multiple, busy, timed-out, or malformed worker captures remain raw
nullable outcomes and make a database-applicable measured run invalid for
comparison. A scenario cancelled before its database phase reports the worker
metric as not applicable rather than manufacturing an idle database request.
Before a measured generation, the M3U benchmark persists and validates its seed
capture, then atomically rolls a clean stopping cutoff into the next active
generation. This closes the otherwise unobservable gap between separate
stop/start calls: pre-rollover requests remain late and fatal, while
post-rollover requests are attributed to the new generation.
### Progress event contract
The worker now emits request-scoped events with:
@@ -568,7 +602,8 @@ executed inside a synchronous transaction callback:
```ts
// favorites is playlist-scoped: filter by (contentId, playlistId), otherwise
// a same-contentId favorite in another playlist gets rewritten too.
const stmt = db.update(schema.favorites)
const stmt = db
.update(schema.favorites)
.set({ position: sql<number>`${sql.placeholder('position')}` })
.where(
and(