mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
* chore(lint): hold tests to their own max-lines ceiling The flat 400-line cap treated a spec like a component. A spec is a flat list of independent cases, so hitting the cap there produces arbitrary `-2.spec.ts` splits and hides coverage instead of surfacing design debt — 65 of the 138 files over the limit were tests. Production code keeps 400. Tests (`**/*.spec.ts`, `**/*.e2e.ts`, and everything under `apps/*-e2e/**`) get 1200. Blank lines and comments no longer count, so a docblock can't be the reason a file must be split. Both limits now live in tools/eslint/max-lines-config.mjs, imported by eslint.config.mjs and the baseline generator alike. The generator decides who belongs on the list by running ESLint's own max-lines rule instead of counting lines itself — a private reimplementation would disagree with the rule the moment either side changed (a `//` inside a template literal is enough) and yield a baseline that turns CI red while looking correct. The baseline drops 126 -> 68 entries with nothing added, and six now-dead `eslint-disable max-lines` directives are removed. A new eslint-tools test asserts the committed baseline still matches what the generator produces, so a stale entry or a forgotten regeneration fails CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(lint): classify eslint-tools in the coverage policy A project with a `test` target must be assigned a coverage tier, so adding eslint-tools broke `coverage:policy:check` before the unit suite even ran. Tier B alongside packaging and release-tools: these are Node tests over lint tooling, and a coverage percentage across a generated list would not mean anything. CI runs Tier B/C through its own `--run-non-tier-a` step, so the baseline-consistency test executes there rather than being skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
461 lines
17 KiB
TypeScript
461 lines
17 KiB
TypeScript
import type {
|
|
CancellationBenchmarkManifest,
|
|
CancellationBenchmarkSummary,
|
|
CancellationIterationResult,
|
|
CancellationPhaseMetrics,
|
|
MainCaptureMetrics,
|
|
MainTimelineRecord,
|
|
NumericDistribution,
|
|
PerformanceWorkerKind,
|
|
RendererCaptureMetrics,
|
|
WorkerRequestPerformanceMetrics,
|
|
} from './m3u-refresh-cancellation-contract';
|
|
import {
|
|
PERFORMANCE_ITERATION_KIND,
|
|
PERFORMANCE_WORKER_KIND,
|
|
} from './m3u-refresh-cancellation-contract';
|
|
import {
|
|
nonNegativeDifference,
|
|
summarizeNumbers,
|
|
} from './performance-statistics';
|
|
import { assessDatabaseWorkerPostGcValidity } from './database-worker-post-gc-validity';
|
|
import { assessRendererRssValidity } from './renderer-rss-validity';
|
|
|
|
export function createCancellationIterationResult(input: {
|
|
readonly kind: CancellationIterationResult['kind'];
|
|
readonly main: MainCaptureMetrics;
|
|
readonly renderer: RendererCaptureMetrics;
|
|
readonly runId: string;
|
|
}): CancellationIterationResult {
|
|
const { main, renderer } = input;
|
|
const phases = derivePhases(main, renderer);
|
|
const cancellationEffectObserved = renderer.probe.events.some(
|
|
(event) => event.status === 'cancelled'
|
|
);
|
|
|
|
return Object.freeze({
|
|
cancellationEffectObserved,
|
|
kind: input.kind,
|
|
main,
|
|
phases,
|
|
renderer,
|
|
runId: input.runId,
|
|
});
|
|
}
|
|
|
|
export function createCancellationBenchmarkSummary(
|
|
manifest: CancellationBenchmarkManifest,
|
|
iterations: readonly CancellationIterationResult[]
|
|
): CancellationBenchmarkSummary {
|
|
const measured = iterations.filter(
|
|
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
|
|
);
|
|
const flattenRenderer = (
|
|
select: (iteration: CancellationIterationResult) => readonly number[]
|
|
): number[] => measured.flatMap((iteration) => [...select(iteration)]);
|
|
const workerMetric = (
|
|
kind: PerformanceWorkerKind,
|
|
select: (worker: MainCaptureMetrics['workers'][number]) => number | null
|
|
): NumericDistribution =>
|
|
summarizeNumbers(
|
|
measured.flatMap((iteration) =>
|
|
iteration.main.workers
|
|
.filter((worker) => worker.kind === kind)
|
|
.map((worker) => select(worker))
|
|
)
|
|
);
|
|
const workerRequestMetric = (
|
|
kind: PerformanceWorkerKind,
|
|
select: (request: WorkerRequestPerformanceMetrics) => number | null
|
|
): NumericDistribution =>
|
|
summarizeNumbers(
|
|
measured.flatMap((iteration) =>
|
|
iteration.main.workers
|
|
.filter((worker) => worker.kind === kind)
|
|
.flatMap((worker) =>
|
|
worker.requests.map((request) => select(request))
|
|
)
|
|
)
|
|
);
|
|
const workerRequestEventLoopDelayMetric = (
|
|
kind: PerformanceWorkerKind,
|
|
percentile: keyof NonNullable<
|
|
WorkerRequestPerformanceMetrics['eventLoopDelay']
|
|
>
|
|
): NumericDistribution =>
|
|
workerRequestMetric(
|
|
kind,
|
|
(request) => request.eventLoopDelay?.[percentile] ?? null
|
|
);
|
|
|
|
return Object.freeze({
|
|
cancellationEffectRate:
|
|
measured.length === 0
|
|
? 0
|
|
: measured.filter(
|
|
(iteration) => iteration.cancellationEffectObserved
|
|
).length / measured.length,
|
|
iterations: Object.freeze([...iterations]),
|
|
manifest,
|
|
measured: Object.freeze({
|
|
cancelAcknowledgementLatencyMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) =>
|
|
iteration.phases.cancelAcknowledgementLatencyMs
|
|
)
|
|
),
|
|
cancelToDurableTerminalMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.phases.cancelToDurableTerminalMs
|
|
)
|
|
),
|
|
cancelToWorkerTerminatedMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.phases.cancelToWorkerTerminatedMs
|
|
)
|
|
),
|
|
cancelTransportLatencyMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.phases.cancelTransportLatencyMs
|
|
)
|
|
),
|
|
databaseWorkerExternalPeakBytes: workerMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
(worker) => worker.peakExternalBytes
|
|
),
|
|
databaseWorkerHeapPeakBytes: workerMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
(worker) => worker.peakHeapUsedBytes
|
|
),
|
|
databaseWorkerPostGcHeapBytes: workerMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
(worker) => worker.postGcHeapUsedBytes
|
|
),
|
|
databaseWorkerRequestEventLoopDelayMaxMs:
|
|
workerRequestEventLoopDelayMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
'maxMs'
|
|
),
|
|
databaseWorkerRequestEventLoopDelayP95Ms:
|
|
workerRequestEventLoopDelayMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
'p95Ms'
|
|
),
|
|
databaseWorkerRequestEventLoopDelayP99Ms:
|
|
workerRequestEventLoopDelayMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
'p99Ms'
|
|
),
|
|
databaseWorkerRequestEventLoopUtilization: workerRequestMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
(request) => request.eventLoopUtilization
|
|
),
|
|
databaseWorkerRequestThreadCpuSystemMicros: workerRequestMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
(request) => request.threadCpuSystemMicros
|
|
),
|
|
databaseWorkerRequestThreadCpuUserMicros: workerRequestMetric(
|
|
PERFORMANCE_WORKER_KIND.DATABASE,
|
|
(request) => request.threadCpuUserMicros
|
|
),
|
|
dataFetchMs: summarizeNumbers(
|
|
measured.map((iteration) => iteration.phases.dataFetchMs)
|
|
),
|
|
dbCommitProxyMs: summarizeNumbers(
|
|
measured.map((iteration) => iteration.phases.dbCommitProxyMs)
|
|
),
|
|
ipcStoreDispatchProxyMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.phases.ipcStoreDispatchProxyMs
|
|
)
|
|
),
|
|
mainEventLoopDelayMaxMs: summarizeNumbers(
|
|
measured.map((iteration) => iteration.main.eventLoopDelay.maxMs)
|
|
),
|
|
mainEventLoopDelayP95Ms: summarizeNumbers(
|
|
measured.map((iteration) => iteration.main.eventLoopDelay.p95Ms)
|
|
),
|
|
mainEventLoopDelayP99Ms: summarizeNumbers(
|
|
measured.map((iteration) => iteration.main.eventLoopDelay.p99Ms)
|
|
),
|
|
mainEventLoopUtilization: summarizeNumbers(
|
|
measured.map((iteration) => iteration.main.eventLoopUtilization)
|
|
),
|
|
mainHeapPeakBytes: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.main.memory.peakHeapUsedBytes
|
|
)
|
|
),
|
|
mainHeapPostGcBytes: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.main.memory.postGcHeapUsedBytes
|
|
)
|
|
),
|
|
mainRssPeakBytes: summarizeNumbers(
|
|
measured.map((iteration) => iteration.main.memory.peakRssBytes)
|
|
),
|
|
mainRssPostGcBytes: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.main.memory.postGcRssBytes
|
|
)
|
|
),
|
|
parseNormalizeCloneProxyMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.phases.parseNormalizeCloneProxyMs
|
|
)
|
|
),
|
|
persistencePreparationProxyMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) =>
|
|
iteration.phases.persistencePreparationProxyMs
|
|
)
|
|
),
|
|
playlistWorkerExternalPeakBytes: workerMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
(worker) => worker.peakExternalBytes
|
|
),
|
|
playlistWorkerHeapPeakBytes: workerMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
(worker) => worker.peakHeapUsedBytes
|
|
),
|
|
playlistWorkerPostGcHeapBytes: workerMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
(worker) => worker.postGcHeapUsedBytes
|
|
),
|
|
playlistWorkerRequestEventLoopDelayMaxMs:
|
|
workerRequestEventLoopDelayMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
'maxMs'
|
|
),
|
|
playlistWorkerRequestEventLoopDelayP95Ms:
|
|
workerRequestEventLoopDelayMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
'p95Ms'
|
|
),
|
|
playlistWorkerRequestEventLoopDelayP99Ms:
|
|
workerRequestEventLoopDelayMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
'p99Ms'
|
|
),
|
|
playlistWorkerRequestEventLoopUtilization: workerRequestMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
(request) => request.eventLoopUtilization
|
|
),
|
|
playlistWorkerRequestThreadCpuSystemMicros: workerRequestMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
(request) => request.threadCpuSystemMicros
|
|
),
|
|
playlistWorkerRequestThreadCpuUserMicros: workerRequestMetric(
|
|
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
|
|
(request) => request.threadCpuUserMicros
|
|
),
|
|
rendererFrameGapMs: summarizeNumbers(
|
|
flattenRenderer(
|
|
(iteration) => iteration.renderer.probe.frameGapsMs
|
|
)
|
|
),
|
|
rendererHeartbeatDelayMs: summarizeNumbers(
|
|
flattenRenderer(
|
|
(iteration) => iteration.renderer.probe.heartbeatDelaysMs
|
|
)
|
|
),
|
|
rendererHeapPeakBytes: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.renderer.peakHeapUsedBytes
|
|
)
|
|
),
|
|
rendererHeapPostGcBytes: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.renderer.postGcHeapUsedBytes
|
|
)
|
|
),
|
|
rendererLongTaskCount: measured.reduce(
|
|
(sum, iteration) =>
|
|
sum + iteration.renderer.probe.longTasksMs.length,
|
|
0
|
|
),
|
|
rendererLongTaskMs: summarizeNumbers(
|
|
flattenRenderer(
|
|
(iteration) => iteration.renderer.probe.longTasksMs
|
|
)
|
|
),
|
|
rendererRssPeakBytes: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) =>
|
|
iteration.main.rendererWindow.rss.peakRssBytes
|
|
)
|
|
),
|
|
responsiveEvents: measured.reduce(
|
|
(sum, iteration) =>
|
|
sum + iteration.main.rendererWindow.responsiveEvents,
|
|
0
|
|
),
|
|
totalMs: summarizeNumbers(
|
|
measured.map((iteration) => iteration.phases.totalMs)
|
|
),
|
|
uiSettlementToPaintMs: summarizeNumbers(
|
|
measured.map(
|
|
(iteration) => iteration.phases.uiSettlementToPaintMs
|
|
)
|
|
),
|
|
unresponsiveEvents: measured.reduce(
|
|
(sum, iteration) =>
|
|
sum + iteration.main.rendererWindow.unresponsiveEvents,
|
|
0
|
|
),
|
|
visibleTotalMs: summarizeNumbers(
|
|
measured.map((iteration) => iteration.phases.visibleTotalMs)
|
|
),
|
|
}),
|
|
validity: Object.freeze({
|
|
databaseWorkerPostGc:
|
|
assessDatabaseWorkerPostGcValidity(iterations),
|
|
rendererRss: assessRendererRssValidity(iterations),
|
|
}),
|
|
});
|
|
}
|
|
|
|
function derivePhases(
|
|
main: MainCaptureMetrics,
|
|
renderer: RendererCaptureMetrics
|
|
): CancellationPhaseMetrics {
|
|
const started = findTimeline(
|
|
main.timeline,
|
|
'playlist-event:started:fetching'
|
|
);
|
|
const parsing = findTimeline(
|
|
main.timeline,
|
|
'playlist-event:progress:parsing'
|
|
);
|
|
const response = findTimeline(main.timeline, 'playlist-response');
|
|
const playlistRequest = findTimeline(main.timeline, 'playlist-request');
|
|
const playlistId = playlistRequest?.playlistId;
|
|
const dbGetRequest = findPlaylistDbRecord(
|
|
main.timeline,
|
|
'db-request',
|
|
response?.epochMs,
|
|
'DB_GET_APP_PLAYLIST',
|
|
playlistId
|
|
);
|
|
const dbGetResponse = findResponse(main.timeline, dbGetRequest);
|
|
const dbUpsertRequest = findPlaylistDbRecord(
|
|
main.timeline,
|
|
'db-request',
|
|
dbGetResponse?.epochMs ?? dbGetRequest?.epochMs ?? response?.epochMs,
|
|
'DB_UPSERT_APP_PLAYLIST',
|
|
playlistId
|
|
);
|
|
const dbUpsertResponse = findResponse(main.timeline, dbUpsertRequest);
|
|
const playlistWorker = main.workers.find(
|
|
(worker) => worker.kind === 'playlist-refresh.worker'
|
|
);
|
|
const cancelClick = renderer.probe.cancelClickEpochMs;
|
|
const uiPainted =
|
|
renderer.probe.uiPaintedEpochMs ?? renderer.probe.terminalEpochMs;
|
|
const cancelledEvent = renderer.probe.events.find(
|
|
(event) => event.status === 'cancelled'
|
|
);
|
|
const terminal = maxEpoch(
|
|
uiPainted,
|
|
dbUpsertResponse?.epochMs,
|
|
cancelledEvent?.receivedEpochMs,
|
|
response?.epochMs,
|
|
playlistWorker?.terminatedEpochMs
|
|
);
|
|
|
|
return Object.freeze({
|
|
cancelAcknowledgementLatencyMs: nonNegativeDifference(
|
|
cancelledEvent?.receivedEpochMs,
|
|
cancelClick
|
|
),
|
|
cancelToDurableTerminalMs: nonNegativeDifference(terminal, cancelClick),
|
|
cancelToWorkerTerminatedMs: nonNegativeDifference(
|
|
playlistWorker?.terminatedEpochMs,
|
|
cancelClick
|
|
),
|
|
cancelTransportLatencyMs: nonNegativeDifference(
|
|
playlistWorker?.cancelPostedEpochMs,
|
|
cancelClick
|
|
),
|
|
dataFetchMs: nonNegativeDifference(parsing?.epochMs, started?.epochMs),
|
|
dbCommitProxyMs: nonNegativeDifference(
|
|
dbUpsertResponse?.epochMs,
|
|
dbUpsertRequest?.epochMs
|
|
),
|
|
ipcStoreDispatchProxyMs: nonNegativeDifference(
|
|
dbGetRequest?.epochMs,
|
|
response?.epochMs
|
|
),
|
|
parseNormalizeCloneProxyMs: nonNegativeDifference(
|
|
response?.epochMs,
|
|
parsing?.epochMs
|
|
),
|
|
persistencePreparationProxyMs: nonNegativeDifference(
|
|
dbUpsertRequest?.epochMs,
|
|
dbGetResponse?.epochMs
|
|
),
|
|
totalMs: nonNegativeDifference(
|
|
terminal,
|
|
renderer.probe.operationStartEpochMs
|
|
),
|
|
uiSettlementToPaintMs: nonNegativeDifference(
|
|
uiPainted,
|
|
renderer.probe.uiSettledEpochMs
|
|
),
|
|
visibleTotalMs: nonNegativeDifference(
|
|
uiPainted,
|
|
renderer.probe.operationStartEpochMs
|
|
),
|
|
});
|
|
}
|
|
|
|
function findTimeline(
|
|
timeline: readonly MainTimelineRecord[],
|
|
type: string
|
|
): MainTimelineRecord | undefined {
|
|
return timeline.find((record) => record.type === type);
|
|
}
|
|
|
|
function findPlaylistDbRecord(
|
|
timeline: readonly MainTimelineRecord[],
|
|
type: string,
|
|
afterEpochMs: number | undefined,
|
|
operation: string,
|
|
playlistId: string | undefined
|
|
): MainTimelineRecord | undefined {
|
|
if (afterEpochMs === undefined) {
|
|
return undefined;
|
|
}
|
|
return timeline.find(
|
|
(record) =>
|
|
record.type === type &&
|
|
record.epochMs >= afterEpochMs &&
|
|
record.operation === operation &&
|
|
(playlistId === undefined || record.playlistId === playlistId)
|
|
);
|
|
}
|
|
|
|
function findResponse(
|
|
timeline: readonly MainTimelineRecord[],
|
|
request: MainTimelineRecord | undefined
|
|
): MainTimelineRecord | undefined {
|
|
if (request?.requestId === undefined) {
|
|
return undefined;
|
|
}
|
|
return timeline.find(
|
|
(record) =>
|
|
record.type === 'db-response' &&
|
|
record.requestId === request.requestId
|
|
);
|
|
}
|
|
|
|
function maxEpoch(
|
|
...values: readonly (number | null | undefined)[]
|
|
): number | null {
|
|
const finite = values.filter(
|
|
(value): value is number =>
|
|
typeof value === 'number' && Number.isFinite(value)
|
|
);
|
|
return finite.length === 0 ? null : Math.max(...finite);
|
|
}
|