test(performance): add end-to-end Xtream benchmark harness (#1300)

* docs(performance): plan Xtream benchmark

* feat(xtream-mock-server): add deterministic 100k fixture

* style(xtream-mock-server): apply repository formatting

* fix(xtream-mock-server): harden performance fixture data

* feat(xtream-mock-server): add performance control plane

* docs(performance): correct Xtream capture plan

* fix(xtream-mock-server): harden performance controls

* fix(xtream-mock-server): harden control lifecycle

* feat(performance): add Xtream preload markers

* feat(performance): trace Xtream main phases

* feat(performance): mark Xtream store publications

* feat(performance): trace Xtream database phases

* feat(performance): trace Xtream delete cancellation

* feat(performance): capture Xtream phase attribution

* feat(performance): mark Sources Xtream refresh

* test(performance): define Xtream benchmark evidence contracts

* test(performance): add Xtream benchmark runner

* test(performance): surface failure evidence writes

* test(performance): align database read clock

* test(performance): preserve capture failure contracts
This commit is contained in:
4gray authored and GitHub committed 2026-07-28 08:08:07 +02:00
1 parent 5e725a06f3
commit a2fafcfc08
256 files changed
+43760 -883

No files matched your search

@@ -23,9 +23,26 @@ import {
PlaylistMeta,
SECURITY_ERROR_PREFIX,
} from '@iptvnator/shared/interfaces';
import {
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
type RendererPerformancePhaseEvent,
} from '@iptvnator/shared/logging';
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
import { PlaylistRefreshActionService } from './playlist-refresh-action.service';
const performanceHookSymbol = Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY);
function setPerformanceHook(
hook: ((event: RendererPerformancePhaseEvent) => void) | null
): void {
const target = globalThis as unknown as Record<symbol, unknown>;
if (hook === null) {
delete target[performanceHookSymbol];
} else {
target[performanceHookSymbol] = hook;
}
}
function createDeferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
@@ -233,6 +250,7 @@ describe('PlaylistRefreshActionService', () => {
});
afterEach(() => {
setPerformanceHook(null);
jest.restoreAllMocks();
localStorage.clear();
});
@@ -382,6 +400,11 @@ describe('PlaylistRefreshActionService', () => {
it('stores Xtream restore data before updating playlist meta and navigating', async () => {
const item = createPlaylistMeta();
const executionOrder: string[] = [];
const performanceEvents: RendererPerformancePhaseEvent[] = [];
setPerformanceHook((event) => {
performanceEvents.push(event);
executionOrder.push(event.boundary);
});
let confirmPromise: Promise<void> | undefined;
const dateNowSpy = jest
.spyOn(Date, 'now')
@@ -456,7 +479,34 @@ describe('PlaylistRefreshActionService', () => {
'xtreams',
item._id,
]);
expect(executionOrder).toEqual(['setItem', 'dispatch', 'navigate']);
expect(executionOrder).toEqual([
'setItem',
'start',
'dispatch',
'end',
'navigate',
]);
expect(
performanceEvents.map(({ boundary, metadata, outcome, phase }) => ({
boundary,
items: metadata?.items,
outcome,
phase,
}))
).toEqual([
{
boundary: 'start',
items: 1,
outcome: undefined,
phase: 'store.xtream-refresh-meta',
},
{
boundary: 'end',
items: 1,
outcome: 'success',
phase: 'store.xtream-refresh-meta',
},
]);
setItemSpy.mockRestore();
dateNowSpy.mockRestore();
@@ -23,6 +23,10 @@ import {
PLAYLIST_UPDATE,
PlaylistMeta,
} from '@iptvnator/shared/interfaces';
import {
measureRendererPerformancePhase,
RENDERER_PERFORMANCE_PHASE,
} from '@iptvnator/shared/logging';
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
export interface XtreamRefreshPreparationState {
@@ -159,10 +163,15 @@ export class PlaylistRefreshActionService {
playbackPositions,
});
this.store.dispatch(
PlaylistActions.updatePlaylistMeta({
playlist: { ...item, updateDate },
})
measureRendererPerformancePhase(
RENDERER_PERFORMANCE_PHASE.XTREAM_REFRESH_META,
() =>
this.store.dispatch(
PlaylistActions.updatePlaylistMeta({
playlist: { ...item, updateDate },
})
),
() => ({ items: 1 })
);
await this.router.navigate([
@@ -27,8 +27,25 @@ import {
SortService,
} from '@iptvnator/services';
import { PLAYLIST_UPDATE, PlaylistMeta } from '@iptvnator/shared/interfaces';
import {
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
type RendererPerformancePhaseEvent,
} from '@iptvnator/shared/logging';
import { RecentPlaylistsComponent } from './recent-playlists.component';
const performanceHookSymbol = Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY);
function setPerformanceHook(
hook: ((event: RendererPerformancePhaseEvent) => void) | null
): void {
const target = globalThis as unknown as Record<symbol, unknown>;
if (hook === null) {
delete target[performanceHookSymbol];
} else {
target[performanceHookSymbol] = hook;
}
}
function createDeferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
@@ -233,11 +250,14 @@ describe('RecentPlaylistsComponent busy state', () => {
});
afterEach(() => {
setPerformanceHook(null);
jest.restoreAllMocks();
localStorage.clear();
});
it('tracks delete progress and clears the busy row after completion', async () => {
const events: RendererPerformancePhaseEvent[] = [];
setPerformanceHook((event) => events.push(event));
const item = createPlaylistMeta({ _id: 'playlist-delete-1' });
const deletion = createDeferred<boolean>();
@@ -290,9 +310,32 @@ describe('RecentPlaylistsComponent busy state', () => {
undefined,
{ duration: 2000 }
);
expect(
events.map(({ boundary, metadata, outcome, phase }) => ({
boundary,
items: metadata?.items,
outcome,
phase,
}))
).toEqual([
{
boundary: 'start',
items: 1,
outcome: undefined,
phase: 'store.xtream-delete-row',
},
{
boundary: 'end',
items: 1,
outcome: 'success',
phase: 'store.xtream-delete-row',
},
]);
});
it('delegates playlist deletion and updates local UI state after success', async () => {
const events: RendererPerformancePhaseEvent[] = [];
setPerformanceHook((event) => events.push(event));
const item = createPlaylistMeta({
_id: 'pwa-playlist-1',
serverUrl: undefined,
@@ -316,6 +359,43 @@ describe('RecentPlaylistsComponent busy state', () => {
undefined,
{ duration: 2000 }
);
expect(events).toEqual([]);
});
it('records dispatch failure and still clears delete-row busy state', async () => {
const events: RendererPerformancePhaseEvent[] = [];
const item = createPlaylistMeta({ _id: 'dispatch-failure' });
const dispatchError = new Error('dispatch failed');
setPerformanceHook((event) => events.push(event));
(store.dispatch as jest.Mock).mockImplementation(() => {
throw dispatchError;
});
await expect(component.removePlaylist(item)).rejects.toBe(
dispatchError
);
expect(component.isDeletePending(item._id)).toBe(false);
expect(component.getBusyProgress(item._id)).toBeNull();
expect(snackBar.open).not.toHaveBeenCalled();
expect(
events.map(({ boundary, outcome, phase }) => ({
boundary,
outcome,
phase,
}))
).toEqual([
{
boundary: 'start',
outcome: undefined,
phase: 'store.xtream-delete-row',
},
{
boundary: 'end',
outcome: 'error',
phase: 'store.xtream-delete-row',
},
]);
});
it('tracks Xtream refresh progress and clears the busy row after abort', async () => {
@@ -388,6 +468,11 @@ describe('RecentPlaylistsComponent busy state', () => {
const item = createPlaylistMeta({ _id: 'playlist-refresh-success-1' });
let confirmPromise: Promise<void> | undefined;
const executionOrder: string[] = [];
const performanceEvents: RendererPerformancePhaseEvent[] = [];
setPerformanceHook((event) => {
performanceEvents.push(event);
executionOrder.push(event.boundary);
});
const dateNowSpy = jest
.spyOn(Date, 'now')
.mockReturnValue(1712145600000);
@@ -476,7 +561,38 @@ describe('RecentPlaylistsComponent busy state', () => {
'xtreams',
item._id,
]);
expect(executionOrder).toEqual(['setItem', 'dispatch', 'navigate']);
expect(executionOrder).toEqual([
'setItem',
'start',
'dispatch',
'end',
'navigate',
]);
expect(performanceEvents).toHaveLength(2);
expect(performanceEvents[0]?.phaseId).toBe(
performanceEvents[1]?.phaseId
);
expect(
performanceEvents.map(({ boundary, metadata, outcome, phase }) => ({
boundary,
metadata,
outcome,
phase,
}))
).toEqual([
{
boundary: 'start',
metadata: { items: 1 },
outcome: undefined,
phase: 'store.xtream-refresh-meta',
},
{
boundary: 'end',
metadata: { items: 1 },
outcome: 'success',
phase: 'store.xtream-refresh-meta',
},
]);
setItemSpy.mockRestore();
dateNowSpy.mockRestore();
@@ -46,6 +46,10 @@ import {
PlaylistMeta,
PlaylistRefreshEvent,
} from '@iptvnator/shared/interfaces';
import {
measureRendererPerformancePhase,
RENDERER_PERFORMANCE_PHASE,
} from '@iptvnator/shared/logging';
import { EmptyStateComponent } from './empty-state/empty-state.component';
import type { PlaylistType } from '../add-playlist-menu/playlist-type';
@@ -271,9 +275,24 @@ export class RecentPlaylistsComponent {
}
);
if (deleted) {
this.store.dispatch(
PlaylistActions.removePlaylist({ playlistId: item._id })
);
if (item.serverUrl) {
measureRendererPerformancePhase(
RENDERER_PERFORMANCE_PHASE.XTREAM_DELETE_ROW,
() =>
this.store.dispatch(
PlaylistActions.removePlaylist({
playlistId: item._id,
})
),
() => ({ items: 1 })
);
} else {
this.store.dispatch(
PlaylistActions.removePlaylist({
playlistId: item._id,
})
);
}
this.snackBar.open(
this.translate.instant(
'HOME.PLAYLISTS.REMOVE_DIALOG.SUCCESS'
@@ -389,10 +408,15 @@ export class RecentPlaylistsComponent {
});
// Update the timestamp in NgRx / IndexedDB
this.store.dispatch(
PlaylistActions.updatePlaylistMeta({
playlist: { ...item, updateDate },
})
measureRendererPerformancePhase(
RENDERER_PERFORMANCE_PHASE.XTREAM_REFRESH_META,
() =>
this.store.dispatch(
PlaylistActions.updatePlaylistMeta({
playlist: { ...item, updateDate },
})
),
() => ({ items: 1 })
);
// Navigate to the playlist to trigger re-import