mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
* test(performance): add formal M3U import benchmark * test(performance): harden formal capture validity * test(performance): address benchmark review feedback
198 lines
6.6 KiB
TypeScript
198 lines
6.6 KiB
TypeScript
import type {
|
|
WorkerCaptureMetrics,
|
|
WorkerRequestPerformanceMetrics,
|
|
} from './m3u-refresh-cancellation-contract';
|
|
import {
|
|
PERFORMANCE_ITERATION_KIND,
|
|
PERFORMANCE_WORKER_KIND,
|
|
} from './m3u-refresh-cancellation-contract';
|
|
|
|
export const DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON = {
|
|
DATABASE_WORKER_MISSING: 'database-worker-missing',
|
|
EVENT_LOOP_DELAY_INVALID: 'event-loop-delay-invalid',
|
|
EVENT_LOOP_UTILIZATION_INVALID: 'event-loop-utilization-invalid',
|
|
MULTIPLE_DATABASE_WORKERS: 'multiple-database-workers',
|
|
REQUEST_SET_INVALID: 'initial-import-request-set-invalid',
|
|
THREAD_CPU_INVALID: 'thread-cpu-invalid',
|
|
WORKER_CAPTURE_INVALID: 'worker-performance-capture-invalid',
|
|
} as const;
|
|
|
|
export type DatabaseWorkerRequestMetricsInvalidReason =
|
|
(typeof DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON)[keyof typeof DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON];
|
|
|
|
export interface DatabaseWorkerRequestMetricsValidity {
|
|
readonly expectedRequestCount: number;
|
|
readonly invalidRequests: readonly {
|
|
readonly operation: string | null;
|
|
readonly reason: DatabaseWorkerRequestMetricsInvalidReason;
|
|
readonly runId: string;
|
|
}[];
|
|
readonly measuredRunCount: number;
|
|
readonly validForComparison: boolean;
|
|
readonly validMeasuredRunCount: number;
|
|
readonly validRequestCount: number;
|
|
}
|
|
|
|
interface RequestMetricsIteration {
|
|
readonly kind: string;
|
|
readonly main: {
|
|
readonly workers: readonly WorkerCaptureMetrics[];
|
|
};
|
|
readonly runId: string;
|
|
}
|
|
|
|
export function assessDatabaseWorkerRequestMetricsValidity(
|
|
iterations: readonly RequestMetricsIteration[]
|
|
): DatabaseWorkerRequestMetricsValidity {
|
|
const measured = iterations.filter(
|
|
(iteration) =>
|
|
iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
|
|
);
|
|
const invalidRequests: {
|
|
operation: string | null;
|
|
reason: DatabaseWorkerRequestMetricsInvalidReason;
|
|
runId: string;
|
|
}[] = [];
|
|
let validMeasuredRunCount = 0;
|
|
let validRequestCount = 0;
|
|
|
|
for (const iteration of measured) {
|
|
const workers = iteration.main.workers.filter(
|
|
(worker) => worker.kind === PERFORMANCE_WORKER_KIND.DATABASE
|
|
);
|
|
if (workers.length === 0) {
|
|
invalidRequests.push({
|
|
operation: null,
|
|
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.DATABASE_WORKER_MISSING,
|
|
runId: iteration.runId,
|
|
});
|
|
continue;
|
|
}
|
|
const worker = workers.length === 1 ? workers[0] : null;
|
|
if (!worker) {
|
|
invalidRequests.push({
|
|
operation: null,
|
|
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.MULTIPLE_DATABASE_WORKERS,
|
|
runId: iteration.runId,
|
|
});
|
|
continue;
|
|
}
|
|
const requests = requireInitialImportRequestPair(worker);
|
|
if (requests === null) {
|
|
invalidRequests.push({
|
|
operation: null,
|
|
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.REQUEST_SET_INVALID,
|
|
runId: iteration.runId,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
let runIsValid = true;
|
|
for (const request of requests) {
|
|
const reason = assessRequest(request);
|
|
if (reason === null) {
|
|
validRequestCount += 1;
|
|
} else {
|
|
runIsValid = false;
|
|
invalidRequests.push({
|
|
operation: request.operation,
|
|
reason,
|
|
runId: iteration.runId,
|
|
});
|
|
}
|
|
}
|
|
if (runIsValid) {
|
|
validMeasuredRunCount += 1;
|
|
}
|
|
}
|
|
|
|
const expectedRequestCount = measured.length * 2;
|
|
return Object.freeze({
|
|
expectedRequestCount,
|
|
invalidRequests: Object.freeze(invalidRequests),
|
|
measuredRunCount: measured.length,
|
|
validForComparison:
|
|
measured.length > 0 &&
|
|
validMeasuredRunCount === measured.length &&
|
|
validRequestCount === expectedRequestCount,
|
|
validMeasuredRunCount,
|
|
validRequestCount,
|
|
});
|
|
}
|
|
|
|
function requireInitialImportRequestPair(
|
|
worker: WorkerCaptureMetrics
|
|
): readonly [
|
|
WorkerRequestPerformanceMetrics,
|
|
WorkerRequestPerformanceMetrics,
|
|
] | null {
|
|
const upserts = worker.requests.filter(
|
|
(request) =>
|
|
request.operation === 'DB_UPSERT_APP_PLAYLIST' && request.success
|
|
);
|
|
const gets = worker.requests.filter(
|
|
(request) =>
|
|
request.operation === 'DB_GET_APP_PLAYLIST' && request.success
|
|
);
|
|
return worker.requests.length === 2 &&
|
|
upserts.length === 1 &&
|
|
upserts[0] &&
|
|
gets.length === 1 &&
|
|
gets[0]
|
|
? [upserts[0], gets[0]]
|
|
: null;
|
|
}
|
|
|
|
function assessRequest(
|
|
request: WorkerRequestPerformanceMetrics
|
|
): DatabaseWorkerRequestMetricsInvalidReason | null {
|
|
if (
|
|
request.performanceCaptureUnavailableReason !== null ||
|
|
request.invalidReason !== null
|
|
) {
|
|
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.WORKER_CAPTURE_INVALID;
|
|
}
|
|
if (
|
|
request.eventLoopDelayUnavailableReason !== null ||
|
|
request.histogramFlushedEpochMs === null ||
|
|
!isEventLoopDelay(request.eventLoopDelay)
|
|
) {
|
|
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_DELAY_INVALID;
|
|
}
|
|
if (
|
|
request.eventLoopUtilizationUnavailableReason !== null ||
|
|
!isUtilization(request.eventLoopUtilization)
|
|
) {
|
|
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_UTILIZATION_INVALID;
|
|
}
|
|
if (
|
|
request.threadCpuUnavailableReason !== null ||
|
|
!isFiniteNonNegative(request.threadCpuSystemMicros) ||
|
|
!isFiniteNonNegative(request.threadCpuUserMicros)
|
|
) {
|
|
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.THREAD_CPU_INVALID;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isEventLoopDelay(
|
|
value: WorkerRequestPerformanceMetrics['eventLoopDelay']
|
|
): value is NonNullable<WorkerRequestPerformanceMetrics['eventLoopDelay']> {
|
|
return (
|
|
value !== null &&
|
|
isFiniteNonNegative(value.maxMs) &&
|
|
isFiniteNonNegative(value.p95Ms) &&
|
|
isFiniteNonNegative(value.p99Ms) &&
|
|
value.p95Ms <= value.p99Ms &&
|
|
value.p99Ms <= value.maxMs
|
|
);
|
|
}
|
|
|
|
function isUtilization(value: number | null): value is number {
|
|
return isFiniteNonNegative(value) && value <= 1;
|
|
}
|
|
|
|
function isFiniteNonNegative(value: unknown): value is number {
|
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
}
|