mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
* 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
286 lines
10 KiB
TypeScript
286 lines
10 KiB
TypeScript
import type { XtreamScenarioId } from './xtream-benchmark-contract';
|
|
|
|
export const XTREAM_ATTRIBUTION_PHASE = {
|
|
CANCEL_SESSION: 'xtream.cancel-session',
|
|
DESERIALIZE_PLAYLIST: 'deserialize.playlist',
|
|
JSON_TRANSFORM: 'xtream.json.transform',
|
|
NETWORK_TOTAL: 'xtream.network.total',
|
|
NORMALIZE_CATEGORIES: 'normalize.categories',
|
|
NORMALIZE_CONTENT: 'normalize.content',
|
|
NORMALIZE_SEARCH_RANK: 'normalize.search-rank',
|
|
RESPONSE_READY: 'xtream.response.ready',
|
|
SERIALIZE_PLAYLIST: 'serialize.playlist',
|
|
SQLITE_CATEGORIES_READ: 'sqlite.categories.read',
|
|
SQLITE_CATEGORIES_WRITE_TRANSACTIONS:
|
|
'sqlite.categories.write-transactions',
|
|
SQLITE_CONTENT_CATEGORY_MAP_READ: 'sqlite.content.category-map-read',
|
|
SQLITE_CONTENT_READ: 'sqlite.content.read',
|
|
SQLITE_CONTENT_WRITE_TRANSACTIONS: 'sqlite.content.write-transactions',
|
|
SQLITE_GENERIC_READ: 'sqlite.read',
|
|
SQLITE_GENERIC_WRITE: 'sqlite.write',
|
|
SQLITE_PLAYLIST_DELETE_COLLECT_IDS: 'sqlite.playlist-delete.collect-ids',
|
|
SQLITE_PLAYLIST_DELETE_WRITE_TRANSACTIONS:
|
|
'sqlite.playlist-delete.write-transactions',
|
|
SQLITE_SEARCH_QUERY: 'sqlite.search.query',
|
|
SQLITE_XTREAM_CACHE_CLEAR_WRITE_TRANSACTIONS:
|
|
'sqlite.xtream-cache-clear.write-transactions',
|
|
SQLITE_XTREAM_DELETE_COLLECT_USER_DATA:
|
|
'sqlite.xtream-delete.collect-user-data',
|
|
SQLITE_XTREAM_DELETE_WRITE_TRANSACTIONS:
|
|
'sqlite.xtream-delete.write-transactions',
|
|
STORE_DELETE_ROW: 'store.xtream-delete-row',
|
|
STORE_IMPORT_TERMINAL: 'store.xtream-import-terminal',
|
|
STORE_PUBLISH_CATEGORIES: 'store.xtream-publish-categories',
|
|
STORE_PUBLISH_LIVE: 'store.xtream-publish-live',
|
|
STORE_PUBLISH_SERIES: 'store.xtream-publish-series',
|
|
STORE_PUBLISH_VOD: 'store.xtream-publish-vod',
|
|
STORE_REFRESH_META: 'store.xtream-refresh-meta',
|
|
STORE_SEARCH_RESULTS: 'store.xtream-search-results',
|
|
} as const;
|
|
|
|
export const XTREAM_PHASE_SEMANTIC_TYPE = {
|
|
ACCOUNT: 'account',
|
|
LIVE_CATEGORIES: 'live-categories',
|
|
LIVE_CONTENT: 'live-content',
|
|
SERIES_CATEGORIES: 'series-categories',
|
|
SERIES_CONTENT: 'series-content',
|
|
VOD_CATEGORIES: 'vod-categories',
|
|
VOD_CONTENT: 'vod-content',
|
|
} as const;
|
|
|
|
export type XtreamPhaseSemanticType =
|
|
(typeof XTREAM_PHASE_SEMANTIC_TYPE)[keyof typeof XTREAM_PHASE_SEMANTIC_TYPE];
|
|
|
|
export interface XtreamInventoryExpectation {
|
|
readonly itemCounts?: readonly (number | null)[];
|
|
readonly occurrences: number;
|
|
}
|
|
|
|
export interface XtreamIpcOutcomeCounts {
|
|
readonly error: number;
|
|
readonly success: number;
|
|
}
|
|
|
|
type AddExpectation = (
|
|
phase: string,
|
|
occurrences: number,
|
|
itemCounts?: readonly (number | null)[]
|
|
) => void;
|
|
|
|
const PHASE = XTREAM_ATTRIBUTION_PHASE;
|
|
|
|
export const XTREAM_NORMALIZATION_PHASES = new Set<string>([
|
|
PHASE.NORMALIZE_CATEGORIES,
|
|
PHASE.NORMALIZE_CONTENT,
|
|
PHASE.NORMALIZE_SEARCH_RANK,
|
|
]);
|
|
export const XTREAM_SQLITE_READ_PHASES = new Set<string>([
|
|
PHASE.SQLITE_CATEGORIES_READ,
|
|
PHASE.SQLITE_CONTENT_CATEGORY_MAP_READ,
|
|
PHASE.SQLITE_CONTENT_READ,
|
|
PHASE.SQLITE_GENERIC_READ,
|
|
PHASE.SQLITE_PLAYLIST_DELETE_COLLECT_IDS,
|
|
PHASE.SQLITE_SEARCH_QUERY,
|
|
PHASE.SQLITE_XTREAM_DELETE_COLLECT_USER_DATA,
|
|
]);
|
|
export const XTREAM_SQLITE_WRITE_PHASES = new Set<string>([
|
|
PHASE.SQLITE_CATEGORIES_WRITE_TRANSACTIONS,
|
|
PHASE.SQLITE_CONTENT_WRITE_TRANSACTIONS,
|
|
PHASE.SQLITE_GENERIC_WRITE,
|
|
PHASE.SQLITE_PLAYLIST_DELETE_WRITE_TRANSACTIONS,
|
|
PHASE.SQLITE_XTREAM_CACHE_CLEAR_WRITE_TRANSACTIONS,
|
|
PHASE.SQLITE_XTREAM_DELETE_WRITE_TRANSACTIONS,
|
|
]);
|
|
|
|
export function matchesXtreamPhaseItemCounts(
|
|
phase: string,
|
|
actual: readonly (number | null)[],
|
|
expected: readonly (number | null)[]
|
|
): boolean {
|
|
const categories = new Set<string>([
|
|
PHASE.NORMALIZE_CATEGORIES,
|
|
PHASE.SQLITE_CATEGORIES_READ,
|
|
PHASE.SQLITE_CATEGORIES_WRITE_TRANSACTIONS,
|
|
]);
|
|
if (!categories.has(phase)) {
|
|
return actual.every((value, index) => value === expected[index]);
|
|
}
|
|
const ordered = (values: readonly (number | null)[]) =>
|
|
[...values].sort(
|
|
(left, right) =>
|
|
(left === null ? -1 : left) - (right === null ? -1 : right)
|
|
);
|
|
return (
|
|
actual.length === expected.length &&
|
|
ordered(actual).every(
|
|
(value, index) => value === ordered(expected)[index]
|
|
)
|
|
);
|
|
}
|
|
|
|
export function expectedXtreamPhases(
|
|
scenarioId: XtreamScenarioId
|
|
): ReadonlyMap<string, XtreamInventoryExpectation> {
|
|
const result = new Map<string, XtreamInventoryExpectation>();
|
|
const add: AddExpectation = (phase, occurrences, itemCounts) =>
|
|
result.set(phase, {
|
|
occurrences,
|
|
...(itemCounts ? { itemCounts } : {}),
|
|
});
|
|
const provider =
|
|
scenarioId === 'xtream-delete-large'
|
|
? 0
|
|
: scenarioId === 'xtream-cancel-import'
|
|
? 6
|
|
: 8;
|
|
for (const phase of [
|
|
PHASE.NETWORK_TOTAL,
|
|
PHASE.JSON_TRANSFORM,
|
|
PHASE.RESPONSE_READY,
|
|
]) {
|
|
add(phase, provider);
|
|
}
|
|
if (scenarioId === 'xtream-delete-large') {
|
|
add(PHASE.SQLITE_PLAYLIST_DELETE_COLLECT_IDS, 2, [100_100, 0]);
|
|
add(PHASE.SQLITE_PLAYLIST_DELETE_WRITE_TRANSACTIONS, 2, [100_101, 1]);
|
|
add(PHASE.STORE_DELETE_ROW, 1, [1]);
|
|
return result;
|
|
}
|
|
add(PHASE.SERIALIZE_PLAYLIST, 1);
|
|
add(PHASE.SQLITE_GENERIC_WRITE, 1);
|
|
const playlistReads =
|
|
scenarioId === 'xtream-refresh-large' ||
|
|
scenarioId === 'xtream-background-ui'
|
|
? 1
|
|
: 0;
|
|
add(PHASE.SQLITE_GENERIC_READ, playlistReads);
|
|
add(PHASE.DESERIALIZE_PLAYLIST, playlistReads);
|
|
const routeHydratesAfterImport =
|
|
scenarioId === 'xtream-refresh-large' ||
|
|
scenarioId === 'xtream-background-ui';
|
|
addFullCategories(add, routeHydratesAfterImport);
|
|
if (scenarioId === 'xtream-cancel-import') {
|
|
add(PHASE.CANCEL_SESSION, 1);
|
|
add(PHASE.SQLITE_CONTENT_READ, 1, [0]);
|
|
add(PHASE.SQLITE_CONTENT_CATEGORY_MAP_READ, 1, [60]);
|
|
add(PHASE.NORMALIZE_CONTENT, 1, [60_000]);
|
|
add(PHASE.SQLITE_CONTENT_WRITE_TRANSACTIONS, 1, [null]);
|
|
add(PHASE.SQLITE_XTREAM_CACHE_CLEAR_WRITE_TRANSACTIONS, 3);
|
|
add(PHASE.STORE_PUBLISH_CATEGORIES, 1, [100]);
|
|
add(PHASE.STORE_IMPORT_TERMINAL, 1, [0]);
|
|
return result;
|
|
}
|
|
if (
|
|
scenarioId === 'xtream-refresh-large' ||
|
|
scenarioId === 'xtream-background-ui'
|
|
) {
|
|
add(PHASE.SQLITE_XTREAM_DELETE_COLLECT_USER_DATA, 1, [100_100]);
|
|
add(PHASE.SQLITE_XTREAM_DELETE_WRITE_TRANSACTIONS, 1, [100_100]);
|
|
add(PHASE.STORE_REFRESH_META, 1, [1]);
|
|
}
|
|
addFullContent(add, routeHydratesAfterImport);
|
|
add(
|
|
PHASE.STORE_PUBLISH_CATEGORIES,
|
|
routeHydratesAfterImport ? 2 : 1,
|
|
routeHydratesAfterImport ? [100, 100] : [100]
|
|
);
|
|
add(
|
|
PHASE.STORE_PUBLISH_LIVE,
|
|
routeHydratesAfterImport ? 2 : 1,
|
|
routeHydratesAfterImport ? [60_000, 60_000] : [60_000]
|
|
);
|
|
add(
|
|
PHASE.STORE_PUBLISH_VOD,
|
|
routeHydratesAfterImport ? 2 : 1,
|
|
routeHydratesAfterImport ? [20_000, 20_000] : [20_000]
|
|
);
|
|
add(
|
|
PHASE.STORE_PUBLISH_SERIES,
|
|
routeHydratesAfterImport ? 2 : 1,
|
|
routeHydratesAfterImport ? [20_000, 20_000] : [20_000]
|
|
);
|
|
add(
|
|
PHASE.STORE_IMPORT_TERMINAL,
|
|
routeHydratesAfterImport ? 2 : 1,
|
|
routeHydratesAfterImport ? [100_000, 100_000] : [100_000]
|
|
);
|
|
return result;
|
|
}
|
|
|
|
export function expectedXtreamIpc(
|
|
scenarioId: XtreamScenarioId
|
|
): ReadonlyMap<string, XtreamIpcOutcomeCounts> {
|
|
const counts =
|
|
scenarioId === 'xtream-delete-large'
|
|
? { dbDeletePlaylist: 2 }
|
|
: scenarioId === 'xtream-cancel-import'
|
|
? {
|
|
dbCancelOperation: 1,
|
|
dbGetCategories: 6,
|
|
dbGetContent: 1,
|
|
dbSaveCategories: 3,
|
|
dbSaveContent: -1,
|
|
dbUpsertAppPlaylist: 1,
|
|
xtreamCancelSession: 1,
|
|
xtreamRequest: 6,
|
|
}
|
|
: scenarioId === 'xtream-refresh-large' ||
|
|
scenarioId === 'xtream-background-ui'
|
|
? {
|
|
dbDeleteXtreamContent: 1,
|
|
dbGetAppPlaylist: 1,
|
|
dbGetCategories: 9,
|
|
dbGetContent: 9,
|
|
dbRestoreXtreamUserData: 1,
|
|
dbSaveCategories: 3,
|
|
dbSaveContent: 3,
|
|
dbUpsertAppPlaylist: 1,
|
|
xtreamRequest: 8,
|
|
}
|
|
: {
|
|
dbGetCategories: 6,
|
|
dbGetContent: 6,
|
|
dbSaveCategories: 3,
|
|
dbSaveContent: 3,
|
|
dbUpsertAppPlaylist: 1,
|
|
xtreamRequest: 8,
|
|
};
|
|
return new Map(
|
|
Object.entries(counts).map(([method, count]) => [
|
|
method,
|
|
{ error: count < 0 ? -count : 0, success: Math.max(0, count) },
|
|
])
|
|
);
|
|
}
|
|
|
|
function addFullCategories(
|
|
add: AddExpectation,
|
|
routeHydratesAfterImport: boolean
|
|
): void {
|
|
add(
|
|
PHASE.SQLITE_CATEGORIES_READ,
|
|
routeHydratesAfterImport ? 9 : 6,
|
|
routeHydratesAfterImport
|
|
? [0, 0, 0, 60, 20, 20, 60, 20, 20]
|
|
: [0, 0, 0, 60, 20, 20]
|
|
);
|
|
add(PHASE.NORMALIZE_CATEGORIES, 3, [60, 20, 20]);
|
|
add(PHASE.SQLITE_CATEGORIES_WRITE_TRANSACTIONS, 3, [60, 20, 20]);
|
|
}
|
|
|
|
function addFullContent(
|
|
add: AddExpectation,
|
|
routeHydratesAfterImport: boolean
|
|
): void {
|
|
add(
|
|
PHASE.SQLITE_CONTENT_READ,
|
|
routeHydratesAfterImport ? 9 : 6,
|
|
routeHydratesAfterImport
|
|
? [0, 60_000, 0, 20_000, 0, 20_000, 60_000, 20_000, 20_000]
|
|
: [0, 60_000, 0, 20_000, 0, 20_000]
|
|
);
|
|
add(PHASE.SQLITE_CONTENT_CATEGORY_MAP_READ, 3, [60, 20, 20]);
|
|
add(PHASE.NORMALIZE_CONTENT, 3, [60_000, 20_000, 20_000]);
|
|
add(PHASE.SQLITE_CONTENT_WRITE_TRANSACTIONS, 3, [60_000, 20_000, 20_000]);
|
|
}
|