mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
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:
1 parent
5e725a06f3
commit
a2fafcfc08
256 files changed
+43760
-883
No files matched your search
@@ -0,0 +1,337 @@
|
||||
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
|
||||
),
|
||||
},
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user