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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

394 lines
13 KiB
TypeScript

/* 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.ok(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 while arming and always emits current DB records', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const start = source.indexOf('const startCapture = async (');
const api = source.indexOf('const api =', start);
const profileReset = source.indexOf('state.mainProfilePath = null', start);
const snapshotReset = source.indexOf(
'state.mainSnapshotPath = null',
start
);
assert.ok(profileReset > start && profileReset < api);
assert.ok(snapshotReset > start && snapshotReset < api);
assert.doesNotMatch(source.slice(start, api), /'Profiler\.start'/);
assert.match(
source,
/record\.captureGeneration === state\.captureGeneration[\s\S]*record\.kind === 'database\.worker'/
);
});