mirror of https://github.com/OpenVidu/openvidu.git
88 lines
2.9 KiB
TypeScript
88 lines
2.9 KiB
TypeScript
import {
|
|
type ExplainSummary,
|
|
explainQuery,
|
|
isAggregate,
|
|
type PlannableQuery,
|
|
summarizeExplain
|
|
} from '@openvidu-dashboard/data-generator/plan';
|
|
import type { Db } from 'mongodb';
|
|
import { expect } from 'vitest';
|
|
|
|
export type { ExplainSummary, PlannableQuery };
|
|
|
|
export interface IndexBackedOptions {
|
|
/** Upper bound of `totalDocsExamined`, or a function of the plan (for instance `(plan) => plan.nReturned`). */
|
|
maxDocsExamined: number | ((plan: ExplainSummary) => number);
|
|
/** A COLLSCAN is accepted while the collection holds at most this many documents. */
|
|
collscanThreshold?: number;
|
|
}
|
|
|
|
const DEFAULT_COLLSCAN_THRESHOLD = 1000;
|
|
|
|
/**
|
|
* Runs `explain('executionStats')` for a find cursor or an aggregation and fails when the winning plan scans a
|
|
* collection above the threshold or examines more documents than the bound. Returns the plan summary.
|
|
*/
|
|
export async function expectIndexBacked(
|
|
db: Db,
|
|
query: PlannableQuery,
|
|
options: IndexBackedOptions
|
|
): Promise<ExplainSummary> {
|
|
const plan = summarizeExplain(await explainQuery(db, query));
|
|
const collection = isAggregate(query) ? query.collection : (query.namespace.collection ?? '?');
|
|
const stages = [...plan.planStages, ...plan.pipelineStages].join(' > ');
|
|
|
|
if (plan.collscan) {
|
|
const documents = await db.collection(collection).estimatedDocumentCount();
|
|
const threshold = options.collscanThreshold ?? DEFAULT_COLLSCAN_THRESHOLD;
|
|
|
|
if (documents > threshold) {
|
|
throw new Error(`${collection}: COLLSCAN over ${String(documents)} documents (${stages})`);
|
|
}
|
|
}
|
|
|
|
const bound =
|
|
typeof options.maxDocsExamined === 'function' ? options.maxDocsExamined(plan) : options.maxDocsExamined;
|
|
const examined = plan.totalDocsExamined;
|
|
|
|
if (examined === null) {
|
|
throw new Error(`${collection}: explain reported no executionStats (${stages})`);
|
|
}
|
|
|
|
if (examined > bound) {
|
|
throw new Error(
|
|
`${collection}: ${String(examined)} documents examined, more than the bound of ${String(bound)} (${stages})`
|
|
);
|
|
}
|
|
|
|
return plan;
|
|
}
|
|
|
|
/** The winning plan uses exactly `expected` and scans no collection. */
|
|
export function expectIndexes(plan: ExplainSummary, expected: string[]): void {
|
|
expect(plan.collscan).toBe(false);
|
|
expect([...new Set(plan.indexes)].sort()).toEqual([...expected].sort());
|
|
}
|
|
|
|
/** A point lookup on `_id`: the ID hack stages name no index. */
|
|
export function expectIdLookup(plan: ExplainSummary): void {
|
|
expect(plan.collscan).toBe(false);
|
|
expect(plan.planStages.some((stage) => stage.endsWith('IDHACK')) || plan.indexes.includes('_id_')).toBe(true);
|
|
}
|
|
|
|
/** p95 of `runs` sequential calls, in milliseconds. */
|
|
export async function p95(run: () => Promise<unknown>, runs = 20): Promise<number> {
|
|
const samples: number[] = [];
|
|
|
|
for (let i = 0; i < runs; i++) {
|
|
const start = performance.now();
|
|
|
|
await run();
|
|
samples.push(performance.now() - start);
|
|
}
|
|
|
|
samples.sort((a, b) => a - b);
|
|
|
|
return samples[Math.ceil(samples.length * 0.95) - 1] ?? Number.NaN;
|
|
}
|