chore(electron): add preload performance markers

This commit is contained in:
4gray committed 2026-07-26 22:37:52 +02:00
1 parent 2cc7d0acb1
commit e3ce60a35e
19 files changed
+2547 -49

No files matched your search

+1
View File
@@ -84,6 +84,7 @@ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC phase markers for refresh/DB benchmark correlation; benchmark tooling sets it explicitly, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only event-loop metrics in database and playlist-refresh worker responses; the performance benchmark sets it automatically, and production launches must leave it unset
- Settings, portal request/response, and trace payloads must use
+1
View File
@@ -141,6 +141,7 @@ Useful narrower flags:
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow navigation/load lifecycle
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console logs into the Electron terminal
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC phase markers for refresh/DB benchmark correlation; benchmark tooling sets it explicitly, and production launches must leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only event-loop metrics in database and playlist-refresh worker responses; the performance benchmark sets it automatically, and production launches must leave it unset
Settings, portal request/response, and trace payloads must use
@@ -0,0 +1,318 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type Playlist,
type PreloadPerformanceMarker,
} from '@iptvnator/shared/interfaces';
import {
createPlaylist,
createPreloadPerformanceHarness,
createRefreshPayload,
expectFixedMarkers,
PRELOAD_ENVIRONMENT,
type PreloadPerformanceHarness,
} from './main.preload.performance.test-helpers';
describe('main preload performance marker contract', () => {
let harness: PreloadPerformanceHarness;
beforeEach(() => {
harness = createPreloadPerformanceHarness();
});
afterEach(() => {
harness.cleanup();
});
async function loadPerformancePreload(): Promise<void> {
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
});
}
it('preserves calls while emitting ordered, correlated, fixed, redacted markers', async () => {
const secrets = {
favorite: 'sentinel-favorite-secret',
filePath: '/sentinel/private/playlist.m3u',
host: 'sentinel.private.example',
item: 'sentinel-item-secret',
result: 'sentinel-result-secret',
title: 'sentinel-title-secret',
url: 'https://sentinel.example/private.m3u?token=secret',
};
const payload = createRefreshPayload({
filePath: secrets.filePath,
title: secrets.title,
trustedInsecureTlsHosts: [secrets.host],
url: secrets.url,
});
const refreshedPlaylist = createPlaylist(payload.playlistId, {
favorites: [secrets.favorite],
items: [{ name: secrets.item }],
title: secrets.title,
url: secrets.url,
});
const storedPlaylist = createPlaylist(payload.playlistId, {
filePath: secrets.filePath,
title: secrets.title,
});
const upsertPlaylist = createPlaylist(payload.playlistId, {
favorites: [secrets.favorite],
items: [{ name: secrets.item }],
title: secrets.title,
});
const upsertResult = {
secretResult: secrets.result,
success: true,
};
await loadPerformancePreload();
harness.ipcRenderer.invoke
.mockResolvedValueOnce(refreshedPlaylist)
.mockResolvedValueOnce(storedPlaylist)
.mockResolvedValueOnce(upsertResult);
const api = harness.getApi();
await expect(api.refreshPlaylist(payload)).resolves.toBe(
refreshedPlaylist
);
await expect(api.dbGetAppPlaylist(payload.playlistId)).resolves.toBe(
storedPlaylist
);
await expect(api.dbUpsertAppPlaylist(upsertPlaylist)).resolves.toBe(
upsertResult
);
expect(harness.ipcRenderer.invoke.mock.calls).toEqual([
['PLAYLIST:REFRESH', payload],
['DB_GET_APP_PLAYLIST', payload.playlistId],
['DB_UPSERT_APP_PLAYLIST', upsertPlaylist],
]);
const markers = harness.getMarkers();
expect(markers).toHaveLength(6);
expect(
markers.map(({ ipcCallId, method, phase }) => [
ipcCallId,
method,
phase,
])
).toEqual([
[1, PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST, 'start'],
[1, PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST, 'success'],
[2, PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST, 'start'],
[2, PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST, 'success'],
[3, PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST, 'start'],
[3, PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST, 'success'],
]);
expect(markers.map(({ correlationState }) => correlationState)).toEqual(
[
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
]
);
expect(markers.map(({ operationId }) => operationId)).toEqual(
Array(6).fill(payload.operationId)
);
expect(markers.map(({ playlistId }) => playlistId)).toEqual(
Array(6).fill(payload.playlistId)
);
expect(markers.map(({ invalidReason }) => invalidReason)).toEqual(
Array(6).fill(null)
);
expectFixedMarkers(markers);
const markerSendOrders = harness.ipcRenderer.send.mock.calls.flatMap(
([channel], index) =>
channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL
? [harness.ipcRenderer.send.mock.invocationCallOrder[index]]
: []
);
const invokeOrders =
harness.ipcRenderer.invoke.mock.invocationCallOrder;
for (const [index, invokeOrder] of invokeOrders.entries()) {
expect(markerSendOrders[index * 2]).toBeLessThan(invokeOrder);
}
const serializedMarkers = JSON.stringify(markers);
for (const secret of Object.values(secrets)) {
expect(serializedMarkers).not.toContain(secret);
}
});
it('preserves rejection identity and emits a redacted error', async () => {
const payload = createRefreshPayload({
title: 'sentinel-rejection-title',
url: 'https://sentinel.example/rejected.m3u',
});
const rejection = new Error('sentinel-rejection-error');
await loadPerformancePreload();
harness.ipcRenderer.invoke.mockRejectedValueOnce(rejection);
await expect(harness.getApi().refreshPlaylist(payload)).rejects.toBe(
rejection
);
const markers = harness.getMarkers();
expect(markerPhases(markers)).toEqual([
[1, 'start'],
[1, 'error'],
]);
expect(markers[1]).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR,
operationId: payload.operationId,
playlistId: payload.playlistId,
});
expect(JSON.stringify(markers)).not.toContain(rejection.message);
expect(JSON.stringify(markers)).not.toContain(payload.title);
expect(JSON.stringify(markers)).not.toContain(payload.url);
});
it('preserves synchronous throw identity and emits a redacted error', async () => {
const synchronousError = new Error('sentinel-synchronous-error');
const payload = createRefreshPayload();
await loadPerformancePreload();
harness.ipcRenderer.invoke.mockImplementationOnce(() => {
throw synchronousError;
});
let caughtError: unknown;
try {
harness.getApi().refreshPlaylist(payload);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBe(synchronousError);
const markers = harness.getMarkers();
expect(markerPhases(markers)).toEqual([
[1, 'start'],
[1, 'error'],
]);
expect(markers[1]).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR,
});
expect(JSON.stringify(markers)).not.toContain(synchronousError.message);
});
it('closes a structured refresh cancellation with a fixed reason', async () => {
const payload = createRefreshPayload();
const cancelledResult = {
operationId: payload.operationId,
type: 'playlist-refresh-cancelled',
} as const;
await loadPerformancePreload();
harness.ipcRenderer.invoke
.mockResolvedValueOnce(cancelledResult)
.mockResolvedValueOnce(null);
const api = harness.getApi();
await expect(api.refreshPlaylist(payload)).resolves.toBe(
cancelledResult
);
await api.dbGetAppPlaylist(payload.playlistId);
const markers = harness.getMarkers();
expect(markers[1]).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.REFRESH_CANCELLED,
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: payload.operationId,
phase: 'success',
playlistId: payload.playlistId,
});
expect(markers[2]).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
});
});
it('uses the exact upsert _id and keeps one malformed reason in both phases', async () => {
const malformedPlaylist = {
...createPlaylist('placeholder'),
_id: {
secret: 'sentinel-malformed-playlist-id',
},
} as unknown as Playlist;
const result = { success: true };
await loadPerformancePreload();
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
await expect(
harness.getApi().dbUpsertAppPlaylist(malformedPlaylist)
).resolves.toBe(result);
expect(harness.ipcRenderer.invoke).toHaveBeenCalledWith(
'DB_UPSERT_APP_PLAYLIST',
malformedPlaylist
);
const markers = harness.getMarkers();
expect(
markers.map(
({ correlationState, invalidReason, phase, playlistId }) => ({
correlationState,
invalidReason,
phase,
playlistId,
})
)
).toEqual([
{
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
phase: 'start',
playlistId: null,
},
{
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
phase: 'success',
playlistId: null,
},
]);
expect(markers[0].ipcCallId).toBe(markers[1].ipcCallId);
expect(JSON.stringify(markers)).not.toContain(
'sentinel-malformed-playlist-id'
);
});
it('keeps results unchanged when marker delivery throws', async () => {
const payload = createRefreshPayload();
const result = createPlaylist(payload.playlistId);
await loadPerformancePreload();
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
harness.ipcRenderer.send.mockImplementation((channel: string) => {
if (channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL) {
throw new Error('marker-send-failed');
}
});
await expect(harness.getApi().refreshPlaylist(payload)).resolves.toBe(
result
);
expect(
harness.ipcRenderer.send.mock.calls.filter(
([channel]) => channel === PRELOAD_PERFORMANCE_MARKER_CHANNEL
)
).toHaveLength(2);
});
});
function markerPhases(
markers: PreloadPerformanceMarker[]
): [number, PreloadPerformanceMarker['phase']][] {
return markers.map(({ ipcCallId, phase }) => [ipcCallId, phase]);
}
@@ -0,0 +1,149 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type Settings,
} from '@iptvnator/shared/interfaces';
import { DEBUG_TRACE_EVENT_CHANNEL } from '../services/debug-trace';
import {
createPreloadPerformanceHarness,
PRELOAD_ENVIRONMENT,
type PreloadPerformanceHarness,
} from './main.preload.performance.test-helpers';
describe('main preload performance marker gates', () => {
let harness: PreloadPerformanceHarness;
beforeEach(() => {
harness = createPreloadPerformanceHarness();
});
afterEach(() => {
harness.cleanup();
});
it.each([undefined, '', '0', 'false', 'no', 'off'])(
'emits no marker when the performance flag is %s',
async (flagValue) => {
await harness.load(
flagValue === undefined
? {}
: {
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: flagValue,
}
);
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(harness.getMarkers()).toEqual([]);
}
);
it.each(['1', ' TRUE ', 'yes', 'On'])(
'accepts the existing true flag value %s',
async (flagValue) => {
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: flagValue,
});
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(
harness.getMarkers().map(({ correlationState, phase }) => ({
correlationState,
phase,
}))
).toEqual([
{
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
phase: 'start',
},
{
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
phase: 'success',
},
]);
}
);
it('keeps trace-only calls on the existing debug channel', async () => {
await harness.load({
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
});
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(
harness
.getSentPayloads<
Record<string, unknown>
>(DEBUG_TRACE_EVENT_CHANNEL)
.map(({ method, phase }) => ({ method, phase }))
).toEqual([
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'start',
},
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'success',
},
]);
expect(harness.getMarkers()).toEqual([]);
});
it('keeps debug tracing unchanged when both gates are enabled', async () => {
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
[PRELOAD_ENVIRONMENT.TRACE_IPC]: '1',
});
await harness.getApi().dbGetAppPlaylist('playlist-1');
expect(
harness
.getSentPayloads<
Record<string, unknown>
>(DEBUG_TRACE_EVENT_CHANNEL)
.map(({ method, phase }) => ({ method, phase }))
).toEqual([
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'start',
},
{
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
phase: 'success',
},
]);
expect(
harness.getSentPayloads(PRELOAD_PERFORMANCE_MARKER_CHANNEL).length
).toBe(2);
});
it('does not inspect or mark non-target calls in performance-only mode', async () => {
const result = { success: true };
const settings = Object.defineProperty({}, 'sentinel', {
enumerable: true,
get: () => {
throw new Error('non-target arguments must stay opaque');
},
}) as Partial<Settings>;
await harness.load({
[PRELOAD_ENVIRONMENT.PERF_CAPTURE]: '1',
});
harness.ipcRenderer.invoke.mockResolvedValueOnce(result);
await expect(harness.getApi().updateSettings(settings)).resolves.toBe(
result
);
expect(harness.ipcRenderer.invoke).toHaveBeenCalledWith(
'SETTINGS_UPDATE',
settings
);
expect(harness.getMarkers()).toEqual([]);
});
});
@@ -0,0 +1,162 @@
import type {
ElectronBridgeApi,
Playlist,
PlaylistRefreshPayload,
PreloadPerformanceMarker,
} from '@iptvnator/shared/interfaces';
export const PRELOAD_ENVIRONMENT = {
PERF_CAPTURE: 'IPTVNATOR_PERF_CAPTURE',
TRACE_IPC: 'IPTVNATOR_TRACE_IPC',
TRACE_STARTUP: 'IPTVNATOR_TRACE_STARTUP',
} as const;
type PreloadEnvironmentName =
(typeof PRELOAD_ENVIRONMENT)[keyof typeof PRELOAD_ENVIRONMENT];
type PreloadEnvironment = Partial<Record<PreloadEnvironmentName, string>>;
export type ExposedElectronApi = ElectronBridgeApi &
Record<string, (...args: unknown[]) => unknown>;
export interface MockIpcRenderer {
invoke: jest.Mock;
off: jest.Mock;
on: jest.Mock;
send: jest.Mock;
}
export interface PreloadPerformanceHarness {
cleanup: () => void;
getApi: () => ExposedElectronApi;
getMarkers: () => PreloadPerformanceMarker[];
getSentPayloads: <T>(channel: string) => T[];
ipcRenderer: MockIpcRenderer;
load: (environment?: PreloadEnvironment) => Promise<void>;
}
const MARKER_KEYS = [
'correlationState',
'invalidReason',
'ipcCallId',
'method',
'operationId',
'phase',
'playlistId',
'sourceEpochMs',
] as const;
export function createPlaylist(
playlistId: string,
overrides: Partial<Playlist> = {}
): Playlist {
return {
_id: playlistId,
autoRefresh: false,
count: 1,
importDate: '2026-07-26T00:00:00.000Z',
lastUsage: '2026-07-26T00:00:00.000Z',
title: 'Performance fixture',
...overrides,
};
}
export function createRefreshPayload(
overrides: Partial<PlaylistRefreshPayload> = {}
): PlaylistRefreshPayload {
return {
operationId: 'operation-1',
playlistId: 'playlist-1',
title: 'Performance fixture',
...overrides,
};
}
export function expectFixedMarkers(markers: PreloadPerformanceMarker[]): void {
for (const [index, marker] of markers.entries()) {
expect(Object.keys(marker).sort()).toEqual([...MARKER_KEYS].sort());
expect(Number.isFinite(marker.sourceEpochMs)).toBe(true);
if (index > 0) {
expect(marker.sourceEpochMs).toBeGreaterThanOrEqual(
markers[index - 1].sourceEpochMs
);
}
}
}
export function createPreloadPerformanceHarness(): PreloadPerformanceHarness {
const originalEnvironment = new Map<
PreloadEnvironmentName,
string | undefined
>(
Object.values(PRELOAD_ENVIRONMENT).map((name) => [
name,
process.env[name],
])
);
let exposedApi: ExposedElectronApi | null = null;
const ipcRenderer: MockIpcRenderer = {
invoke: jest.fn(),
off: jest.fn(),
on: jest.fn(),
send: jest.fn(),
};
function getSentPayloads<T>(channel: string): T[] {
return ipcRenderer.send.mock.calls
.filter(([sentChannel]) => sentChannel === channel)
.map(([, payload]) => payload as T);
}
return {
cleanup: () => {
for (const [name, value] of originalEnvironment) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
jest.dontMock('electron');
},
getApi: () => {
if (!exposedApi) {
throw new Error('Expected preload API to be exposed');
}
return exposedApi;
},
getMarkers: () =>
getSentPayloads<PreloadPerformanceMarker>(
'IPTVNATOR_PERF_CAPTURE_MARKER'
),
getSentPayloads,
ipcRenderer,
load: async (environment = {}) => {
jest.resetModules();
for (const name of Object.values(PRELOAD_ENVIRONMENT)) {
delete process.env[name];
}
Object.assign(process.env, environment);
exposedApi = null;
ipcRenderer.invoke.mockReset().mockResolvedValue({ success: true });
ipcRenderer.off.mockReset();
ipcRenderer.on.mockReset();
ipcRenderer.send.mockReset();
jest.doMock('electron', () => ({
contextBridge: {
exposeInMainWorld: jest.fn(
(_name: string, api: ExposedElectronApi) => {
exposedApi = api;
}
),
},
ipcRenderer,
webUtils: {
getPathForFile: jest.fn(),
},
}));
await import('./main.preload');
},
};
}
@@ -11,6 +11,10 @@ import {
attachEmbeddedMpvFrameView,
detachEmbeddedMpvFrameView,
} from './embedded-mpv-frame-pump';
import {
createPreloadPerformanceCapture,
toPreloadPerformanceTargetMethod,
} from './preload-performance-capture';
import type {
EmbeddedMpvBounds,
EmbeddedMpvRecordingStartOptions,
@@ -48,6 +52,7 @@ import type {
} from '@iptvnator/shared/interfaces';
import {
DEBUG_TRACE_EVENT_CHANNEL,
isPerformanceCaptureEnabled,
isRendererApiTraceEnabled,
roundTraceDuration,
summarizeForTrace,
@@ -72,6 +77,11 @@ const dbSaveContentProgressListeners = new Set<
>();
const shouldTraceRendererApi = isRendererApiTraceEnabled();
const shouldCapturePerformance = isPerformanceCaptureEnabled();
const preloadPerformanceCapture = createPreloadPerformanceCapture(
shouldCapturePerformance,
(channel, marker) => ipcRenderer.send(channel, marker)
);
function emitRendererTrace(payload: {
method: string;
@@ -89,7 +99,7 @@ function emitRendererTrace(payload: {
}
function wrapElectronApi<T extends object>(api: T): T {
if (!shouldTraceRendererApi) {
if (!shouldTraceRendererApi && !shouldCapturePerformance) {
return api;
}
@@ -98,7 +108,9 @@ function wrapElectronApi<T extends object>(api: T): T {
if (
typeof value !== 'function' ||
name.startsWith('on') ||
name.startsWith('remove')
name.startsWith('remove') ||
(!shouldTraceRendererApi &&
!toPreloadPerformanceTargetMethod(name))
) {
return [name, value];
}
@@ -108,13 +120,20 @@ function wrapElectronApi<T extends object>(api: T): T {
return [
name,
(...args: unknown[]) => {
const startedAt =
globalThis.performance?.now?.() ?? Date.now();
emitRendererTrace({
args: summarizeForTrace(args),
method: name,
phase: 'start',
});
const startedAt = shouldTraceRendererApi
? (globalThis.performance?.now?.() ?? Date.now())
: 0;
if (shouldTraceRendererApi) {
emitRendererTrace({
args: summarizeForTrace(args),
method: name,
phase: 'start',
});
}
const performanceCall = preloadPerformanceCapture.start(
name,
args
);
try {
const result = original(...args);
@@ -126,54 +145,74 @@ function wrapElectronApi<T extends object>(api: T): T {
) {
return (result as Promise<unknown>)
.then((resolvedValue) => {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(
resolvedValue
),
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(
resolvedValue
),
});
}
preloadPerformanceCapture.success(
performanceCall,
resolvedValue
);
return resolvedValue;
})
.catch((error: unknown) => {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
}
preloadPerformanceCapture.error(
performanceCall
);
throw error;
});
}
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(result),
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
method: name,
phase: 'success',
result: summarizeForTrace(result),
});
}
preloadPerformanceCapture.success(
performanceCall,
result
);
return result;
} catch (error) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
if (shouldTraceRendererApi) {
emitRendererTrace({
durationMs: roundTraceDuration(
(globalThis.performance?.now?.() ??
Date.now()) - startedAt
),
error: summarizeForTrace(error),
method: name,
phase: 'error',
});
}
preloadPerformanceCapture.error(performanceCall);
throw error;
}
},
@@ -530,8 +569,16 @@ const electronApi: ElectronBridgeApi = {
ipcRenderer.invoke('EPG_MAPPING_GET', { channelKey }),
getEpgMappingsBatch: (channelKeys: string[]) =>
ipcRenderer.invoke('EPG_MAPPING_GET_BATCH', { channelKeys }),
setEpgMapping: (channelKey: string, epgChannelId: string, playlistId?: string) =>
ipcRenderer.invoke('EPG_MAPPING_SET', { channelKey, epgChannelId, playlistId }),
setEpgMapping: (
channelKey: string,
epgChannelId: string,
playlistId?: string
) =>
ipcRenderer.invoke('EPG_MAPPING_SET', {
channelKey,
epgChannelId,
playlistId,
}),
deleteEpgMapping: (channelKey: string) =>
ipcRenderer.invoke('EPG_MAPPING_DELETE', { channelKey }),
searchEpgChannels: (searchTerm: string, limit?: number) =>
@@ -0,0 +1,50 @@
import {
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type PreloadPerformanceMarker,
} from '@iptvnator/shared/interfaces';
import { createPreloadPerformanceCapture } from './preload-performance-capture';
describe('preload performance capture timestamps', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('uses fractional monotonic time on top of the epoch origin', () => {
const markers: PreloadPerformanceMarker[] = [];
const monotonicNow = jest
.spyOn(globalThis.performance, 'now')
.mockReturnValueOnce(10.125)
.mockReturnValueOnce(10.375);
const capture = createPreloadPerformanceCapture(
true,
(channel, marker) => {
expect(channel).toBe(PRELOAD_PERFORMANCE_MARKER_CHANNEL);
markers.push(marker);
}
);
const call = capture.start(
PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
[
{
operationId: 'operation-1',
playlistId: 'playlist-1',
},
]
);
capture.success(call, {});
expect(monotonicNow).toHaveBeenCalledTimes(2);
expect(markers).toHaveLength(2);
expect(
markers[0].sourceEpochMs - globalThis.performance.timeOrigin
).toBeCloseTo(10.125, 3);
expect(
markers[1].sourceEpochMs - globalThis.performance.timeOrigin
).toBeCloseTo(10.375, 3);
expect(markers[1].sourceEpochMs).toBeGreaterThan(
markers[0].sourceEpochMs
);
});
});
@@ -0,0 +1,191 @@
import {
isPlaylistRefreshCancelledResult,
PRELOAD_PERFORMANCE_MARKER_CHANNEL,
PRELOAD_PERFORMANCE_METHOD,
type PreloadPerformanceMarker,
type PreloadPerformanceMethod,
type PreloadPerformancePhase,
} from '@iptvnator/shared/interfaces';
import {
advancePreloadPerformanceCorrelation,
createPreloadPerformanceCorrelationState,
normalizePreloadPerformanceIdentifier,
type PreloadPerformanceCorrelationState,
} from './preload-performance-correlation';
export interface PreloadPerformanceCallContext {
ipcCallId: number;
method: PreloadPerformanceMethod;
operationId: string | null;
playlistId: string | null;
}
export interface PreloadPerformanceCapture {
error: (call: PreloadPerformanceCallContext | null) => void;
start: (
methodName: string,
args: readonly unknown[]
) => PreloadPerformanceCallContext | null;
success: (
call: PreloadPerformanceCallContext | null,
result: unknown
) => void;
}
type MarkerSender = (
channel: typeof PRELOAD_PERFORMANCE_MARKER_CHANNEL,
marker: PreloadPerformanceMarker
) => void;
function readProperty(value: unknown, property: string): unknown {
if (
(typeof value !== 'object' || value === null) &&
typeof value !== 'function'
) {
return undefined;
}
try {
return Reflect.get(value, property);
} catch {
return undefined;
}
}
export function toPreloadPerformanceTargetMethod(
methodName: string
): PreloadPerformanceMethod | null {
switch (methodName) {
case PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST:
case PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST:
case PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST:
return methodName;
default:
return null;
}
}
function readHighResolutionEpochMs(): number {
const timeOrigin = globalThis.performance?.timeOrigin;
const monotonicNow = globalThis.performance?.now?.();
const highResolutionEpoch =
typeof timeOrigin === 'number' && typeof monotonicNow === 'number'
? timeOrigin + monotonicNow
: Number.NaN;
return Number.isFinite(highResolutionEpoch)
? highResolutionEpoch
: Date.now();
}
function extractIdentifiers(
method: PreloadPerformanceMethod,
args: readonly unknown[]
): Pick<PreloadPerformanceCallContext, 'operationId' | 'playlistId'> {
if (method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
const payload = args[0];
return {
operationId: normalizePreloadPerformanceIdentifier(
readProperty(payload, 'operationId')
),
playlistId: normalizePreloadPerformanceIdentifier(
readProperty(payload, 'playlistId')
),
};
}
if (method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST) {
return {
operationId: null,
playlistId: normalizePreloadPerformanceIdentifier(args[0]),
};
}
return {
operationId: null,
playlistId: normalizePreloadPerformanceIdentifier(
readProperty(args[0], '_id')
),
};
}
export function createPreloadPerformanceCapture(
enabled: boolean,
sendMarker: MarkerSender
): PreloadPerformanceCapture {
let correlationState: PreloadPerformanceCorrelationState =
createPreloadPerformanceCorrelationState();
let lastSourceEpochMs = 0;
let nextIpcCallId = 1;
function emit(
call: PreloadPerformanceCallContext,
phase: PreloadPerformancePhase,
refreshCancelled = false
): void {
try {
const transition = advancePreloadPerformanceCorrelation(
correlationState,
{
...call,
phase,
refreshCancelled,
}
);
correlationState = transition.state;
const sourceEpochMs = Math.max(
readHighResolutionEpochMs(),
lastSourceEpochMs
);
lastSourceEpochMs = sourceEpochMs;
sendMarker(PRELOAD_PERFORMANCE_MARKER_CHANNEL, {
...transition.marker,
sourceEpochMs,
});
} catch {
// Performance instrumentation must never affect the bridge call.
}
}
return {
error: (call) => {
if (call) {
emit(call, 'error');
}
},
start: (methodName, args) => {
if (!enabled) {
return null;
}
const method = toPreloadPerformanceTargetMethod(methodName);
if (!method) {
return null;
}
const call: PreloadPerformanceCallContext = {
ipcCallId: nextIpcCallId,
method,
...extractIdentifiers(method, args),
};
nextIpcCallId += 1;
emit(call, 'start');
return call;
},
success: (call, result) => {
if (!call) {
return;
}
let refreshCancelled = false;
if (call.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
try {
refreshCancelled = isPlaylistRefreshCancelledResult(result);
} catch {
refreshCancelled = false;
}
}
emit(call, 'success', refreshCancelled);
},
};
}
@@ -0,0 +1,235 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
PRELOAD_PERFORMANCE_SEQUENCE_STAGE,
closeInvalidPreloadPerformanceSequenceWhenSettled,
createPreloadPerformanceMarkerMetadata,
invalidatePreloadPerformanceSequence,
type PreloadPerformanceCall,
type PreloadPerformanceCorrelationEvent,
type PreloadPerformanceMarkerMetadata,
type PreloadPerformanceSequence,
} from './preload-performance-correlation.model';
function invalidCompletion(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
call: PreloadPerformanceCall
): PreloadPerformanceMarkerMetadata {
const invalidSequence =
call.playlistId === null
? undefined
: invalidatePreloadPerformanceSequence(
calls,
sequences,
call.playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
calls.delete(event.ipcCallId);
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
call.playlistId
);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
invalidSequence?.operationId ?? call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidSequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
}
function completeCorrelatedCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
call: PreloadPerformanceCall & { playlistId: string }
): PreloadPerformanceMarkerMetadata {
const sequence = sequences.get(call.playlistId);
const expectedStage =
event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_STARTED
: event.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_STARTED
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_UPSERT_STARTED;
if (
!sequence ||
sequence.stage !== expectedStage ||
sequence.activeCallId !== event.ipcCallId ||
sequence.operationId !== call.operationId
) {
return invalidCompletion(calls, sequences, event, call);
}
if (event.phase === 'error') {
const invalidSequence = invalidatePreloadPerformanceSequence(
calls,
sequences,
call.playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR
);
calls.delete(event.ipcCallId);
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
call.playlistId
);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidSequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR
);
}
if (
event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST &&
event.refreshCancelled === true
) {
sequences.delete(call.playlistId);
calls.delete(event.ipcCallId);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
PRELOAD_PERFORMANCE_INVALID_REASON.REFRESH_CANCELLED
);
}
calls.delete(event.ipcCallId);
if (event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST) {
sequences.set(call.playlistId, {
...sequence,
activeCallId: null,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_SUCCEEDED,
});
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
if (event.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST) {
sequences.set(call.playlistId, {
...sequence,
activeCallId: null,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_SUCCEEDED,
});
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
sequences.delete(call.playlistId);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
null
);
}
export function completePreloadPerformanceCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null
): PreloadPerformanceMarkerMetadata {
const call = calls.get(event.ipcCallId);
if (!call) {
const sequence = playlistId ? sequences.get(playlistId) : undefined;
const invalidSequence = playlistId
? invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
)
: undefined;
const invalidReason =
invalidSequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER;
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
playlistId
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
invalidSequence?.operationId ?? sequence?.operationId ?? null,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason
);
}
const identityMatches =
call.method === event.method &&
call.playlistId === playlistId &&
(event.method !== PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST ||
call.expectedOperationId === operationId);
if (!identityMatches) {
return invalidCompletion(calls, sequences, event, call);
}
if (
call.correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED
) {
calls.delete(event.ipcCallId);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
null,
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
null
);
}
if (
call.correlationState === PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID
) {
calls.delete(event.ipcCallId);
closeInvalidPreloadPerformanceSequenceWhenSettled(
calls,
sequences,
call.playlistId
);
return createPreloadPerformanceMarkerMetadata(
event,
call.playlistId,
call.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
call.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
}
if (call.playlistId === null) {
return invalidCompletion(calls, sequences, event, call);
}
return completeCorrelatedCall(
calls,
sequences,
event,
call as PreloadPerformanceCall & { playlistId: string }
);
}
@@ -0,0 +1,314 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createCorrelationHarness,
OPERATION_ONE,
OPERATION_TWO,
PLAYLIST_ONE,
PLAYLIST_TWO,
type CorrelationHarness,
} from './preload-performance-correlation.test-helpers';
describe('preload performance marker correlation completions', () => {
let advance: CorrelationHarness['advance'];
let reset: CorrelationHarness['reset'];
beforeEach(() => {
({ advance, reset } = createCorrelationHarness());
});
it.each(['success', 'error'] as const)(
'rejects a %s completion with the wrong call ID',
(phase) => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const mismatchedCompletion = advance({
ipcCallId: 99,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase,
playlistId: PLAYLIST_ONE,
});
const originalCompletion = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(mismatchedCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
expect(originalCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
}
);
it('rejects a refresh completion whose playlist or operation ID changed', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const changedOperation = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(changedOperation).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
reset();
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const changedPlaylist = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_TWO,
});
expect(changedPlaylist).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
});
it('rejects a completion whose method differs from the started call', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const wrongMethod = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const restart = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(wrongMethod).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
expect(restart.correlationState).toBe(
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
);
});
it('rejects a duplicate completion instead of advancing the chain twice', () => {
const refresh = {
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
} as const;
advance({ ...refresh, phase: 'start' });
advance({ ...refresh, phase: 'success' });
const duplicate = advance({ ...refresh, phase: 'success' });
const laterGet = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(duplicate).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
expect(laterGet).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
});
});
it('rejects a database completion for a different playlist', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const changedPlaylist = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_TWO,
});
const laterUpsert = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(changedPlaylist).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
expect(laterUpsert).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
});
});
it('invalidates a correlated sequence when a target call errors', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const error = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'error',
playlistId: PLAYLIST_ONE,
});
const laterUpsert = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const restart = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(error).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.IPC_ERROR,
operationId: OPERATION_ONE,
});
expect(laterUpsert).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
});
expect(restart.correlationState).toBe(
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
);
});
it('closes a cancelled refresh without awaiting database persistence', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const cancelled = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
refreshCancelled: true,
});
const laterGet = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(cancelled).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.REFRESH_CANCELLED,
operationId: OPERATION_ONE,
});
expect(laterGet).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
operationId: null,
});
});
});
@@ -0,0 +1,178 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
type PreloadPerformanceCorrelationStateName,
type PreloadPerformanceInvalidReason,
type PreloadPerformanceMarker,
type PreloadPerformanceMethod,
type PreloadPerformancePhase,
} from '@iptvnator/shared/interfaces';
const MAX_IDENTIFIER_LENGTH = 128;
const SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
export const PRELOAD_PERFORMANCE_SEQUENCE_STAGE = {
DB_GET_STARTED: 'db-get-started',
DB_GET_SUCCEEDED: 'db-get-succeeded',
DB_UPSERT_STARTED: 'db-upsert-started',
INVALID: 'invalid',
REFRESH_STARTED: 'refresh-started',
REFRESH_SUCCEEDED: 'refresh-succeeded',
} as const;
export type PreloadPerformanceSequenceStage =
(typeof PRELOAD_PERFORMANCE_SEQUENCE_STAGE)[keyof typeof PRELOAD_PERFORMANCE_SEQUENCE_STAGE];
export interface PreloadPerformanceSequence {
activeCallId: number | null;
invalidReason: PreloadPerformanceInvalidReason | null;
operationId: string;
stage: PreloadPerformanceSequenceStage;
}
export interface PreloadPerformanceCall {
correlationState: PreloadPerformanceCorrelationStateName;
expectedOperationId: string | null;
invalidReason: PreloadPerformanceInvalidReason | null;
method: PreloadPerformanceMethod;
operationId: string | null;
playlistId: string | null;
}
export interface PreloadPerformanceCorrelationEvent {
ipcCallId: number;
method: PreloadPerformanceMethod;
operationId: unknown;
phase: PreloadPerformancePhase;
playlistId: unknown;
refreshCancelled?: boolean;
}
export interface PreloadPerformanceCorrelationState {
readonly calls: ReadonlyMap<number, PreloadPerformanceCall>;
readonly sequences: ReadonlyMap<string, PreloadPerformanceSequence>;
}
export interface PreloadPerformanceCorrelationResult {
marker: PreloadPerformanceMarkerMetadata;
state: PreloadPerformanceCorrelationState;
}
export type PreloadPerformanceMarkerMetadata = Omit<
PreloadPerformanceMarker,
'sourceEpochMs'
>;
export function normalizePreloadPerformanceIdentifier(
value: unknown
): string | null {
if (
typeof value !== 'string' ||
value.length === 0 ||
value.length > MAX_IDENTIFIER_LENGTH ||
value !== value.trim() ||
!SAFE_IDENTIFIER_PATTERN.test(value)
) {
return null;
}
return value;
}
export function createPreloadPerformanceMarkerMetadata(
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null,
correlationState: PreloadPerformanceCorrelationStateName,
invalidReason: PreloadPerformanceInvalidReason | null
): PreloadPerformanceMarkerMetadata {
return {
correlationState,
invalidReason,
ipcCallId: event.ipcCallId,
method: event.method,
operationId,
phase: event.phase,
playlistId,
};
}
export function createInvalidPreloadPerformanceCall(
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null,
invalidReason: PreloadPerformanceInvalidReason,
expectedOperationId = operationId
): PreloadPerformanceCall {
return {
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
expectedOperationId,
invalidReason,
method: event.method,
operationId,
playlistId,
};
}
export function invalidatePreloadPerformanceSequence(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
playlistId: string,
reason: PreloadPerformanceInvalidReason
): PreloadPerformanceSequence | undefined {
const sequence = sequences.get(playlistId);
if (!sequence) {
return undefined;
}
const invalidReason =
sequence.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
? (sequence.invalidReason ?? reason)
: reason;
const invalidSequence: PreloadPerformanceSequence = {
...sequence,
activeCallId: null,
invalidReason,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID,
};
sequences.set(playlistId, invalidSequence);
for (const [callId, call] of calls) {
if (
call.playlistId === playlistId &&
call.correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
) {
calls.set(callId, {
...call,
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason,
});
}
}
return invalidSequence;
}
export function closeInvalidPreloadPerformanceSequenceWhenSettled(
calls: ReadonlyMap<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
playlistId: string | null
): void {
if (
playlistId === null ||
sequences.get(playlistId)?.stage !==
PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
) {
return;
}
const hasInvalidCall = Array.from(calls.values()).some(
(call) =>
call.playlistId === playlistId &&
call.correlationState ===
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID
);
if (!hasInvalidCall) {
sequences.delete(playlistId);
}
}
@@ -0,0 +1,338 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createCorrelationHarness,
OPERATION_ONE,
OPERATION_TWO,
PLAYLIST_ONE,
PLAYLIST_TWO,
type CorrelationEvent,
type CorrelationHarness,
} from './preload-performance-correlation.test-helpers';
describe('preload performance marker correlation sequences', () => {
let advance: CorrelationHarness['advance'];
let reset: CorrelationHarness['reset'];
beforeEach(() => {
({ advance, reset } = createCorrelationHarness());
});
it('correlates the successful refresh persistence sequence and propagates its operation ID', () => {
const refreshStart = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const refreshSuccess = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const getStart = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const getSuccess = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const upsertStart = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const upsertSuccess = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(refreshStart).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
operationId: OPERATION_ONE,
playlistId: PLAYLIST_ONE,
});
expect(refreshSuccess).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
operationId: OPERATION_ONE,
});
expect(getStart.operationId).toBe(OPERATION_ONE);
expect(getSuccess.operationId).toBe(OPERATION_ONE);
expect(upsertStart.operationId).toBe(OPERATION_ONE);
expect(upsertSuccess).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.COMPLETE,
invalidReason: null,
operationId: OPERATION_ONE,
});
});
it('fails closed for wrong-order and duplicate targeted database calls', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const wrongOrder = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(wrongOrder).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
operationId: OPERATION_ONE,
});
reset();
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const duplicate = advance({
ipcCallId: 5,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const originalCompletion = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(duplicate.invalidReason).toBe(
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
expect(originalCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason: PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER,
});
});
it('invalidates concurrent refreshes for the same playlist', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const concurrentStart = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const firstCompletion = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const concurrentCompletion = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
const restart = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: 'operation-3',
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(concurrentStart).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH,
operationId: OPERATION_TWO,
});
expect(concurrentCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH,
operationId: OPERATION_TWO,
});
expect(restart.correlationState).toBe(
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED
);
expect(firstCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH,
});
});
it('keeps different playlists independent when one sequence invalidates', () => {
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_TWO,
});
advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'success',
playlistId: PLAYLIST_TWO,
});
advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const playlistTwoGet = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_TWO,
});
expect(playlistTwoGet).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
operationId: OPERATION_TWO,
playlistId: PLAYLIST_TWO,
});
});
it('allows a new refresh after the prior sequence completes', () => {
const events: CorrelationEvent[] = [
{
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
},
{
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
},
];
events.forEach((event) => advance(event));
const restart = advance({
ipcCallId: 4,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_TWO,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
expect(restart).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
invalidReason: null,
operationId: OPERATION_TWO,
});
});
});
@@ -0,0 +1,251 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
PRELOAD_PERFORMANCE_SEQUENCE_STAGE,
createInvalidPreloadPerformanceCall,
createPreloadPerformanceMarkerMetadata,
invalidatePreloadPerformanceSequence,
type PreloadPerformanceCall,
type PreloadPerformanceCorrelationEvent,
type PreloadPerformanceMarkerMetadata,
type PreloadPerformanceSequence,
} from './preload-performance-correlation.model';
function startRefresh(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string,
operationId: string
): PreloadPerformanceMarkerMetadata {
const sequence = sequences.get(playlistId);
if (sequence) {
const invalidSequence = invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
sequence.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID
? (sequence.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER)
: PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH
);
const invalidReason =
invalidSequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.CONCURRENT_REFRESH;
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
operationId,
invalidReason,
operationId
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason
);
}
sequences.set(playlistId, {
activeCallId: event.ipcCallId,
invalidReason: null,
operationId,
stage: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_STARTED,
});
calls.set(event.ipcCallId, {
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
expectedOperationId: operationId,
invalidReason: null,
method: event.method,
operationId,
playlistId,
});
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
function startDatabaseCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string
): PreloadPerformanceMarkerMetadata {
const sequence = sequences.get(playlistId);
if (!sequence) {
calls.set(event.ipcCallId, {
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
expectedOperationId: null,
invalidReason: null,
method: event.method,
operationId: null,
playlistId,
});
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
null,
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
null
);
}
if (sequence.stage === PRELOAD_PERFORMANCE_SEQUENCE_STAGE.INVALID) {
const invalidReason =
sequence.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER;
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
sequence.operationId,
invalidReason,
null
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason
);
}
const isGet =
event.method === PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST;
const expectedStage = isGet
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.REFRESH_SUCCEEDED
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_SUCCEEDED;
const startedStage = isGet
? PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_GET_STARTED
: PRELOAD_PERFORMANCE_SEQUENCE_STAGE.DB_UPSERT_STARTED;
if (sequence.stage !== expectedStage) {
const invalidSequence = invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
const invalidReason =
invalidSequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER;
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
sequence.operationId,
invalidReason,
null
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason
);
}
sequences.set(playlistId, {
...sequence,
activeCallId: event.ipcCallId,
stage: startedStage,
});
calls.set(event.ipcCallId, {
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
expectedOperationId: null,
invalidReason: null,
method: event.method,
operationId: sequence.operationId,
playlistId,
});
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
sequence.operationId,
PRELOAD_PERFORMANCE_CORRELATION_STATE.CORRELATED,
null
);
}
export function startPreloadPerformanceCall(
calls: Map<number, PreloadPerformanceCall>,
sequences: Map<string, PreloadPerformanceSequence>,
event: PreloadPerformanceCorrelationEvent,
playlistId: string | null,
operationId: string | null
): PreloadPerformanceMarkerMetadata {
if (
playlistId === null ||
(event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST &&
operationId === null)
) {
if (playlistId !== null) {
invalidatePreloadPerformanceSequence(
calls,
sequences,
playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER
);
}
calls.set(
event.ipcCallId,
createInvalidPreloadPerformanceCall(
event,
playlistId,
null,
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId
)
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
null,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER
);
}
const existingCall = calls.get(event.ipcCallId);
if (existingCall) {
const sequence =
existingCall.playlistId === null
? undefined
: invalidatePreloadPerformanceSequence(
calls,
sequences,
existingCall.playlistId,
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
return createPreloadPerformanceMarkerMetadata(
event,
playlistId,
sequence?.operationId ?? null,
PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
sequence?.invalidReason ??
PRELOAD_PERFORMANCE_INVALID_REASON.OUT_OF_ORDER
);
}
return event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
? startRefresh(calls, sequences, event, playlistId, operationId)
: startDatabaseCall(calls, sequences, event, playlistId);
}
@@ -0,0 +1,29 @@
import {
advancePreloadPerformanceCorrelation,
createPreloadPerformanceCorrelationState,
type PreloadPerformanceCorrelationEvent,
} from './preload-performance-correlation';
export const PLAYLIST_ONE = 'playlist-1';
export const PLAYLIST_TWO = 'playlist-2';
export const OPERATION_ONE = 'operation-1';
export const OPERATION_TWO = 'operation-2';
export type CorrelationEvent = PreloadPerformanceCorrelationEvent;
export function createCorrelationHarness() {
let state = createPreloadPerformanceCorrelationState();
return {
advance: (event: PreloadPerformanceCorrelationEvent) => {
const result = advancePreloadPerformanceCorrelation(state, event);
state = result.state;
return result.marker;
},
reset: () => {
state = createPreloadPerformanceCorrelationState();
},
};
}
export type CorrelationHarness = ReturnType<typeof createCorrelationHarness>;
@@ -0,0 +1,60 @@
import { PRELOAD_PERFORMANCE_METHOD } from '@iptvnator/shared/interfaces';
import { completePreloadPerformanceCall } from './preload-performance-correlation.complete';
import {
normalizePreloadPerformanceIdentifier,
type PreloadPerformanceCorrelationEvent,
type PreloadPerformanceCorrelationResult,
type PreloadPerformanceCorrelationState,
} from './preload-performance-correlation.model';
import { startPreloadPerformanceCall } from './preload-performance-correlation.start';
export type {
PreloadPerformanceCorrelationEvent,
PreloadPerformanceCorrelationResult,
PreloadPerformanceCorrelationState,
} from './preload-performance-correlation.model';
export { normalizePreloadPerformanceIdentifier } from './preload-performance-correlation.model';
export function createPreloadPerformanceCorrelationState(): PreloadPerformanceCorrelationState {
return {
calls: new Map(),
sequences: new Map(),
};
}
export function advancePreloadPerformanceCorrelation(
state: PreloadPerformanceCorrelationState,
event: PreloadPerformanceCorrelationEvent
): PreloadPerformanceCorrelationResult {
const calls = new Map(state.calls);
const sequences = new Map(state.sequences);
const playlistId = normalizePreloadPerformanceIdentifier(event.playlistId);
const operationId =
event.method === PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST
? normalizePreloadPerformanceIdentifier(event.operationId)
: null;
const marker =
event.phase === 'start'
? startPreloadPerformanceCall(
calls,
sequences,
event,
playlistId,
operationId
)
: completePreloadPerformanceCall(
calls,
sequences,
event,
playlistId,
operationId
);
return {
marker,
state: {
calls,
sequences,
},
};
}
@@ -0,0 +1,117 @@
import {
PRELOAD_PERFORMANCE_CORRELATION_STATE,
PRELOAD_PERFORMANCE_INVALID_REASON,
PRELOAD_PERFORMANCE_METHOD,
} from '@iptvnator/shared/interfaces';
import {
createCorrelationHarness,
OPERATION_ONE,
OPERATION_TWO,
PLAYLIST_ONE,
type CorrelationHarness,
} from './preload-performance-correlation.test-helpers';
describe('preload performance marker correlation validation', () => {
let advance: CorrelationHarness['advance'];
beforeEach(() => {
({ advance } = createCorrelationHarness());
});
it('uses null identifiers and a fixed reason for malformed inputs', () => {
const malformedPlaylist = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: 'https://secret.example/private.m3u',
});
const malformedOperation = advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: { secret: OPERATION_TWO },
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const malformedDbId = advance({
ipcCallId: 3,
method: PRELOAD_PERFORMANCE_METHOD.DB_UPSERT_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: undefined,
});
const malformedPlaylistCompletion = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'success',
playlistId: 'https://secret.example/private.m3u',
});
expect(malformedPlaylist).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: null,
});
expect(malformedOperation).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: PLAYLIST_ONE,
});
expect(malformedDbId).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: null,
});
expect(malformedPlaylistCompletion).toMatchObject({
correlationState: PRELOAD_PERFORMANCE_CORRELATION_STATE.INVALID,
invalidReason:
PRELOAD_PERFORMANCE_INVALID_REASON.MALFORMED_IDENTIFIER,
operationId: null,
playlistId: null,
});
});
it('keeps targeted database calls outside a sequence visible and uncorrelated', () => {
const getStart = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
advance({
ipcCallId: 2,
method: PRELOAD_PERFORMANCE_METHOD.REFRESH_PLAYLIST,
operationId: OPERATION_ONE,
phase: 'start',
playlistId: PLAYLIST_ONE,
});
const getSuccess = advance({
ipcCallId: 1,
method: PRELOAD_PERFORMANCE_METHOD.DB_GET_APP_PLAYLIST,
operationId: null,
phase: 'success',
playlistId: PLAYLIST_ONE,
});
expect(getStart).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
operationId: null,
});
expect(getSuccess).toMatchObject({
correlationState:
PRELOAD_PERFORMANCE_CORRELATION_STATE.UNCORRELATED,
invalidReason: null,
operationId: null,
});
});
});
@@ -52,6 +52,10 @@ export function isRendererApiTraceEnabled(): boolean {
return isStartupTraceEnabled() || readFlag('IPTVNATOR_TRACE_IPC');
}
export function isPerformanceCaptureEnabled(): boolean {
return readFlag('IPTVNATOR_PERF_CAPTURE');
}
export function isDbTraceEnabled(): boolean {
return isStartupTraceEnabled() || readFlag('IPTVNATOR_TRACE_DB');
}
+1
View File
@@ -23,6 +23,7 @@ export * from './lib/playback-position.interface';
export * from './lib/playlist-auto-update.interface';
export * from './lib/playlist-backup.interface';
export * from './lib/playlist-meta.type';
export * from './lib/preload-performance-marker.interface';
export * from './lib/playlist-recently-viewed.interface';
export * from './lib/playlist-recently-viewed.utils';
export * from './lib/playlist-refresh.interface';
@@ -0,0 +1,52 @@
export const PRELOAD_PERFORMANCE_MARKER_CHANNEL =
'IPTVNATOR_PERF_CAPTURE_MARKER';
export const PRELOAD_PERFORMANCE_METHOD = {
DB_GET_APP_PLAYLIST: 'dbGetAppPlaylist',
DB_UPSERT_APP_PLAYLIST: 'dbUpsertAppPlaylist',
REFRESH_PLAYLIST: 'refreshPlaylist',
} as const;
export type PreloadPerformanceMethod =
(typeof PRELOAD_PERFORMANCE_METHOD)[keyof typeof PRELOAD_PERFORMANCE_METHOD];
export const PRELOAD_PERFORMANCE_PHASE = {
ERROR: 'error',
START: 'start',
SUCCESS: 'success',
} as const;
export type PreloadPerformancePhase =
(typeof PRELOAD_PERFORMANCE_PHASE)[keyof typeof PRELOAD_PERFORMANCE_PHASE];
export const PRELOAD_PERFORMANCE_CORRELATION_STATE = {
COMPLETE: 'complete',
CORRELATED: 'correlated',
INVALID: 'invalid',
UNCORRELATED: 'uncorrelated',
} as const;
export type PreloadPerformanceCorrelationStateName =
(typeof PRELOAD_PERFORMANCE_CORRELATION_STATE)[keyof typeof PRELOAD_PERFORMANCE_CORRELATION_STATE];
export const PRELOAD_PERFORMANCE_INVALID_REASON = {
CONCURRENT_REFRESH: 'concurrent-refresh',
IPC_ERROR: 'ipc-error',
MALFORMED_IDENTIFIER: 'malformed-identifier',
OUT_OF_ORDER: 'out-of-order',
REFRESH_CANCELLED: 'refresh-cancelled',
} as const;
export type PreloadPerformanceInvalidReason =
(typeof PRELOAD_PERFORMANCE_INVALID_REASON)[keyof typeof PRELOAD_PERFORMANCE_INVALID_REASON];
export interface PreloadPerformanceMarker {
method: PreloadPerformanceMethod;
phase: PreloadPerformancePhase;
sourceEpochMs: number;
ipcCallId: number;
playlistId: string | null;
operationId: string | null;
correlationState: PreloadPerformanceCorrelationStateName;
invalidReason: PreloadPerformanceInvalidReason | null;
}