mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
* test(performance): harden Xtream startup retry * test(performance): preserve Xtream teardown failures * test(performance): retry Xtream profile cleanup
340 lines
12 KiB
TypeScript
340 lines
12 KiB
TypeScript
import {
|
|
XTREAM_ITERATION_KIND,
|
|
type XtreamIterationDefinition,
|
|
type XtreamScenarioId,
|
|
} from './xtream-benchmark-contract';
|
|
import { iteration } from './xtream-benchmark-report.fixtures';
|
|
import type {
|
|
WorkerCaptureMetrics,
|
|
WorkerRequestPerformanceMetrics,
|
|
} from './m3u-refresh-cancellation-contract';
|
|
import type {
|
|
XtreamIpcAttributionSpan,
|
|
XtreamPhaseAttributionInput,
|
|
} from './xtream-phase-attribution';
|
|
import { XTREAM_ATTRIBUTION_PHASE as PHASE } from './xtream-phase-inventory';
|
|
import {
|
|
XTREAM_IPC_PRODUCER_NAMESPACE,
|
|
xtreamIpcProducerNamespaceForMethod,
|
|
xtreamIpcProducerNamespaceForWorkerOperation,
|
|
} from './xtream-ipc-producer-identity';
|
|
import type { XtreamIterationAssemblyInput } from './xtream-iteration-assembler';
|
|
import {
|
|
createBackgroundEvidence,
|
|
createLegacyTimeline,
|
|
createRendererCapture,
|
|
createTerminal,
|
|
operationOutcomes,
|
|
scenarioOrdinal,
|
|
} from './xtream-iteration-assembler-fixture-runtime';
|
|
import type { XtreamIpcTimelineRecord } from './xtream-ipc-marker-events.model';
|
|
import type { XtreamRawIterationResult } from './xtream-summary-contract';
|
|
|
|
const MAIN_PHASES = new Set<string>([
|
|
PHASE.CANCEL_SESSION,
|
|
PHASE.JSON_TRANSFORM,
|
|
PHASE.NETWORK_TOTAL,
|
|
PHASE.RESPONSE_READY,
|
|
]);
|
|
|
|
export function createXtreamAssemblerFixture(
|
|
scenarioId: XtreamScenarioId
|
|
): XtreamIterationAssemblyInput {
|
|
const definition: XtreamIterationDefinition = {
|
|
eligibleForComparison: true,
|
|
kind: XTREAM_ITERATION_KIND.MEASURED,
|
|
runId: 'run-01',
|
|
scenarioId,
|
|
};
|
|
const raw = iteration(definition, scenarioOrdinal(scenarioId));
|
|
const worker = createWorker(raw);
|
|
const background = createBackgroundEvidence(raw, worker.requests);
|
|
const captureStartedEpochMs = raw.phaseCapture.operationStartEpochMs - 1;
|
|
const captureCutoffEpochMs = raw.phaseCapture.terminalEpochMs + 1;
|
|
const rendererCapture = createRendererCapture(raw, background?.result);
|
|
return {
|
|
catalogVerification: {
|
|
databaseStateValid: true,
|
|
fixtureIdentityValid: true,
|
|
providerCategoryCount:
|
|
raw.catalogVerification.providerCategoryCount,
|
|
providerItemCount: raw.catalogVerification.providerItemCount,
|
|
verifiedAfterCapture: true,
|
|
},
|
|
definition,
|
|
diagnosticArtifacts: null,
|
|
mainCapture: {
|
|
cancelTimeline:
|
|
raw.cancellationEvidence?.timeline.map((entry) => ({
|
|
...entry,
|
|
})) ?? [],
|
|
capture: {
|
|
cpuProfilePath: null,
|
|
cpuSystemMicros: raw.main.cpuSystemMicros,
|
|
cpuUserMicros: raw.main.cpuUserMicros,
|
|
eventLoopDelay: {
|
|
maxMs: raw.main.eventLoopDelayMaxMs,
|
|
p95Ms: raw.main.eventLoopDelayP95Ms,
|
|
p99Ms: raw.main.eventLoopDelayP99Ms,
|
|
},
|
|
eventLoopUtilization: null,
|
|
eventLoopUtilizationUnavailableReason:
|
|
raw.main.eventLoopUtilizationUnavailableReason,
|
|
heapSnapshotPath: null,
|
|
memory: {
|
|
peakHeapUsedBytes: raw.main.peakHeapUsedBytes,
|
|
peakRssBytes: raw.main.peakRssBytes,
|
|
postGcHeapUsedBytes: raw.main.postGcHeapUsedBytes,
|
|
postGcRssBytes: raw.main.peakRssBytes - 1,
|
|
},
|
|
rendererWindow: {
|
|
responsiveEvents: raw.main.responsiveEvents,
|
|
rss: {
|
|
identity: { creationTime: 1, pid: 42 },
|
|
missingSampleCount: 0,
|
|
peakRssBytes: raw.renderer.peakRssBytes,
|
|
unavailableReason: null,
|
|
validSampleCount: 2,
|
|
},
|
|
unresponsiveEvents: raw.main.unresponsiveEvents,
|
|
windowIdentity: {
|
|
browserWindowId: raw.main.browserWindowId,
|
|
webContentsId: raw.main.webContentsId,
|
|
},
|
|
},
|
|
rssScope:
|
|
'electron-main-process-including-worker-threads-and-native-memory',
|
|
timeline: createLegacyTimeline(worker.requests),
|
|
workers: [worker],
|
|
},
|
|
captureCutoffEpochMs,
|
|
captureGeneration: 1,
|
|
captureStartedEpochMs,
|
|
captureStoppedEpochMs: captureCutoffEpochMs + 1,
|
|
databaseWorkerCount: 1,
|
|
invalidReasons: {
|
|
capture: [],
|
|
databaseWorkerCancel: [],
|
|
xtreamIpc: [],
|
|
},
|
|
ipcTimeline: createXtreamTimeline(raw),
|
|
lateDatabaseRequestCount: 0,
|
|
lateEventCount: 0,
|
|
measurementStartedEpochMs:
|
|
raw.phaseCapture.operationStartEpochMs - 0.5,
|
|
operationOutcomes: operationOutcomes(worker.requests),
|
|
playlistRefreshWorkerCount: 0,
|
|
quarantinedIpcMarkerCount: 0,
|
|
requests: worker.requests,
|
|
rollover: null,
|
|
workers: [worker],
|
|
},
|
|
process: {
|
|
electronPid: 20_000 + scenarioOrdinal(scenarioId),
|
|
freshProcessVerified: true,
|
|
launchId: `launch-${scenarioOrdinal(scenarioId)}`,
|
|
profileDirectorySha256: String(scenarioOrdinal(scenarioId)).repeat(
|
|
64
|
|
),
|
|
startupAttemptCount: 1,
|
|
startupRetryReasons: [],
|
|
},
|
|
rendererCapture,
|
|
terminal: createTerminal(raw, background),
|
|
trigger: {
|
|
scenarioId,
|
|
triggerEpochMs: raw.phaseCapture.operationStartEpochMs + 0.1,
|
|
},
|
|
};
|
|
}
|
|
|
|
function createWorker(raw: XtreamRawIterationResult): WorkerCaptureMetrics {
|
|
const metric = raw.databaseWorker;
|
|
return {
|
|
cancelPostedEpochMs: raw.cancellation.databaseDispatchEpochMs,
|
|
cpuSystemMicros: metric.cpuSystemMicros,
|
|
cpuUserMicros: metric.cpuUserMicros,
|
|
eventLoopDelay: {
|
|
maxMs: metric.eventLoopDelayMaxMs,
|
|
p95Ms: metric.eventLoopDelayP95Ms,
|
|
p99Ms: metric.eventLoopDelayP99Ms,
|
|
},
|
|
eventLoopDelayUnavailableReason: null,
|
|
eventLoopUtilization: metric.eventLoopUtilization,
|
|
externalMemorySampleCount: 2,
|
|
heapUsedSampleCount: 2,
|
|
kind: 'database.worker',
|
|
operationId: null,
|
|
ordinal: metric.ordinal,
|
|
peakExternalBytes: metric.peakExternalBytes,
|
|
peakExternalUnavailableReason: null,
|
|
peakHeapUsedBytes: metric.peakHeapUsedBytes,
|
|
peakHeapUsedUnavailableReason: null,
|
|
playlistId: null,
|
|
postGcHeapUnavailableReason: null,
|
|
postGcHeapUsedBytes: metric.postGcHeapUsedBytes,
|
|
profilePath: null,
|
|
requests: metric.requests.evidence.map(cloneRequest),
|
|
responseEpochMs: null,
|
|
snapshotPath: null,
|
|
terminatedEpochMs: null,
|
|
};
|
|
}
|
|
|
|
function cloneRequest(
|
|
request: WorkerRequestPerformanceMetrics
|
|
): WorkerRequestPerformanceMetrics {
|
|
return {
|
|
...request,
|
|
eventLoopDelay:
|
|
request.eventLoopDelay === null
|
|
? null
|
|
: { ...request.eventLoopDelay },
|
|
phaseEvents: request.phaseEvents.map((event) => ({
|
|
...event,
|
|
...(event.metadata ? { metadata: { ...event.metadata } } : {}),
|
|
})),
|
|
};
|
|
}
|
|
|
|
function createXtreamTimeline(
|
|
raw: XtreamRawIterationResult
|
|
): XtreamIpcTimelineRecord[] {
|
|
const main = raw.phaseCapture.events
|
|
.filter(({ phase }) => MAIN_PHASES.has(phase))
|
|
.map((event): XtreamIpcTimelineRecord => ({
|
|
action: event.providerAction,
|
|
boundary: event.boundary,
|
|
categoryType: event.categoryType,
|
|
contentType: event.contentType,
|
|
durationMs: event.durationMs,
|
|
epochMs: event.epochMs,
|
|
phase: event.phase,
|
|
requestId: event.correlationId,
|
|
type: 'xtream-main-phase',
|
|
}));
|
|
const markers = xtreamCalls(raw).flatMap(({ request, response }) => [
|
|
marker(raw, request, 'start'),
|
|
marker(raw, response, 'end'),
|
|
]);
|
|
return [...main, ...markers].sort((left, right) => {
|
|
return left.epochMs - right.epochMs;
|
|
});
|
|
}
|
|
|
|
function marker(
|
|
raw: XtreamRawIterationResult,
|
|
span: XtreamIpcAttributionSpan,
|
|
boundary: 'end' | 'start'
|
|
): XtreamIpcTimelineRecord {
|
|
const semantic = markerSemantic(raw, span);
|
|
return {
|
|
action: semantic.providerAction,
|
|
boundary,
|
|
categoryType: semantic.categoryType,
|
|
contentType: semantic.contentType,
|
|
epochMs: span.endEpochMs,
|
|
ipcCallId: span.ipcCallId,
|
|
itemCount: null,
|
|
method: span.method,
|
|
operationId:
|
|
span.method === 'dbCancelOperation'
|
|
? raw.cancellation.failedOperationId
|
|
: semantic.operationId,
|
|
outcome: boundary === 'start' ? null : span.outcome,
|
|
playlistId: semantic.playlistId,
|
|
sessionId: span.method.startsWith('xtream') ? 'session-1' : null,
|
|
sourceEpochMs:
|
|
boundary === 'start' ? span.sourceEpochMs : span.startEpochMs,
|
|
type: 'xtream-preload-marker',
|
|
};
|
|
}
|
|
|
|
function markerSemantic(
|
|
raw: XtreamRawIterationResult,
|
|
span: XtreamIpcAttributionSpan
|
|
) {
|
|
if (span.method === 'xtreamRequest') {
|
|
const providers = providerGroups(raw.phaseCapture);
|
|
const calls = xtreamCalls(raw).filter(
|
|
({ request }) => request.method === 'xtreamRequest'
|
|
);
|
|
const index = calls.findIndex(
|
|
({ request }) => request.ipcCallId === span.ipcCallId
|
|
);
|
|
const provider = providers[index]?.[0];
|
|
return {
|
|
categoryType: null,
|
|
contentType: null,
|
|
operationId: null,
|
|
playlistId: null,
|
|
providerAction: provider?.providerAction ?? null,
|
|
};
|
|
}
|
|
const request = raw.databaseWorker.requests.evidence.find(
|
|
(candidate) =>
|
|
candidate.ipcCallId === span.ipcCallId &&
|
|
xtreamIpcProducerNamespaceForWorkerOperation(
|
|
candidate.operation ?? ''
|
|
) === XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD
|
|
);
|
|
const event = raw.phaseCapture.events.find(
|
|
(candidate) =>
|
|
candidate.correlationId === request?.requestId &&
|
|
candidate.boundary === 'start'
|
|
);
|
|
return {
|
|
categoryType: event?.categoryType ?? null,
|
|
contentType: event?.contentType ?? null,
|
|
operationId: request?.operationId ?? null,
|
|
playlistId: request?.playlistId ?? null,
|
|
providerAction: null,
|
|
};
|
|
}
|
|
|
|
function xtreamCalls(raw: XtreamRawIterationResult) {
|
|
return pairIpc(raw.phaseCapture.ipcSpans).filter(
|
|
({ request }) =>
|
|
xtreamIpcProducerNamespaceForMethod(request.method) ===
|
|
XTREAM_IPC_PRODUCER_NAMESPACE.XTREAM_PRELOAD
|
|
);
|
|
}
|
|
|
|
function pairIpc(spans: readonly XtreamIpcAttributionSpan[]) {
|
|
const groups = new Map<string, XtreamIpcAttributionSpan[]>();
|
|
for (const span of spans) {
|
|
groups.set(span.correlationId, [
|
|
...(groups.get(span.correlationId) ?? []),
|
|
span,
|
|
]);
|
|
}
|
|
return [...groups.values()].map((group) => {
|
|
const request = group.find(({ boundary }) => boundary === 'request');
|
|
const response = group.find(({ boundary }) => boundary === 'response');
|
|
if (!request || !response) {
|
|
throw new Error('invalid Xtream assembler IPC fixture');
|
|
}
|
|
return { request, response };
|
|
});
|
|
}
|
|
|
|
function providerGroups(capture: XtreamPhaseAttributionInput) {
|
|
const groups = new Map<string, typeof capture.events>();
|
|
for (const event of capture.events.filter(
|
|
({ phase }) => phase === PHASE.NETWORK_TOTAL
|
|
)) {
|
|
groups.set(event.correlationId, [
|
|
...(groups.get(event.correlationId) ?? []),
|
|
event,
|
|
]);
|
|
}
|
|
return [...groups.values()].sort((left, right) => {
|
|
const leftStart = left[0];
|
|
const rightStart = right[0];
|
|
if (!leftStart || !rightStart) {
|
|
throw new Error('invalid Xtream assembler provider fixture');
|
|
}
|
|
return leftStart.epochMs - rightStart.epochMs;
|
|
});
|
|
}
|