mirror of https://github.com/OpenVidu/openvidu.git
160 lines
6.0 KiB
TypeScript
160 lines
6.0 KiB
TypeScript
|
|
import { writeFileSync } from 'node:fs';
|
||
|
|
|
||
|
|
import { generate } from '@openvidu-dashboard/data-generator/generator';
|
||
|
|
import { summarizeExplain } from '@openvidu-dashboard/data-generator/plan';
|
||
|
|
import { type Db, type Document, MongoClient } from 'mongodb';
|
||
|
|
import { afterAll, beforeAll, describe, expect, inject, it } from 'vitest';
|
||
|
|
|
||
|
|
import { Collection, freshnessWindows } from '@openvidu-dashboard/typings';
|
||
|
|
|
||
|
|
import {
|
||
|
|
activeParticipantsPipeline,
|
||
|
|
activeRoomsPipeline,
|
||
|
|
IN_FLIGHT_EGRESS,
|
||
|
|
PUBLISHING_INGRESS,
|
||
|
|
OverviewRepository,
|
||
|
|
RECENT_ROOMS
|
||
|
|
} from '../../src/overview/overview-repository.js';
|
||
|
|
import { OverviewService } from '../../src/overview/overview-service.js';
|
||
|
|
import { todayWindow } from '../../src/overview/overview-window.js';
|
||
|
|
import { ROOM_LIST_PROJECTION } from '../../src/rooms/rooms-query.js';
|
||
|
|
import { expectIndexBacked, expectIndexes, p95 } from '../support/query-plan.js';
|
||
|
|
import { expectWithinBudgets } from '../support/latency-budget.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Every overview query explained on a database generated by the data generator, plus the p95 of the performance budget,
|
||
|
|
* which fails the suite when exceeded. OVERVIEW_PLAN_SCENARIO (S by default), OVERVIEW_PLAN_DAYS (2 by default) and
|
||
|
|
* OVERVIEW_PLAN_REPORT (a file that also receives the report) work as in the rooms suite.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const mongoUri = inject('mongoUri');
|
||
|
|
const scenario = (process.env.OVERVIEW_PLAN_SCENARIO ?? 'S') as 'S' | 'M' | 'L';
|
||
|
|
const days = Number(process.env.OVERVIEW_PLAN_DAYS ?? '2');
|
||
|
|
const DATABASE = 'dashboard_test_overview_plan';
|
||
|
|
const GENERATION_TIMEOUT_MS = scenario === 'S' ? 600_000 : 3_600_000;
|
||
|
|
|
||
|
|
describe.skipIf(mongoUri === '')(`overview query plan at scenario ${scenario}`, () => {
|
||
|
|
let client: MongoClient;
|
||
|
|
let db: Db;
|
||
|
|
let service: OverviewService;
|
||
|
|
const now = new Date();
|
||
|
|
const threshold = new Date(now.getTime() - freshnessWindows().staleMs);
|
||
|
|
const report: string[] = [];
|
||
|
|
|
||
|
|
const aggregate = (collection: Collection, pipeline: Document[], hint?: string) =>
|
||
|
|
db
|
||
|
|
.collection(collection)
|
||
|
|
.aggregate(pipeline, hint === undefined ? {} : { hint })
|
||
|
|
.explain('executionStats')
|
||
|
|
.then(summarizeExplain);
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
client = new MongoClient(mongoUri);
|
||
|
|
db = client.db(DATABASE);
|
||
|
|
await db.dropDatabase();
|
||
|
|
await generate(db, {
|
||
|
|
scenario,
|
||
|
|
days,
|
||
|
|
statsMode: 'participant',
|
||
|
|
seed: 1,
|
||
|
|
retentionDays: { events: 7, stats: 7, tracks: 7, entities: 90 },
|
||
|
|
timeseries: true
|
||
|
|
});
|
||
|
|
service = new OverviewService(db, { now: () => now, extension: () => ({}) });
|
||
|
|
}, GENERATION_TIMEOUT_MS);
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
if (report.length > 0) {
|
||
|
|
const text = [`Overview p95 of 20 runs, scenario ${scenario}, ${String(days)} days`, ...report].join('\n');
|
||
|
|
|
||
|
|
console.log(text);
|
||
|
|
|
||
|
|
if (process.env.OVERVIEW_PLAN_REPORT !== undefined) {
|
||
|
|
writeFileSync(process.env.OVERVIEW_PLAN_REPORT, `${text}\n`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
await db.dropDatabase();
|
||
|
|
await client.close();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('active groups examine exactly the active rooms and participants', async () => {
|
||
|
|
const activeRooms = await db.collection(Collection.ROOMS).countDocuments({ status: 'active' });
|
||
|
|
const activeParticipants = await db.collection(Collection.PARTICIPANTS).countDocuments({ status: 'active' });
|
||
|
|
const rooms = await aggregate(Collection.ROOMS, activeRoomsPipeline(threshold), 'status_createdAt_id');
|
||
|
|
const participants = await aggregate(Collection.PARTICIPANTS, activeParticipantsPipeline(threshold));
|
||
|
|
|
||
|
|
expect(activeRooms).toBeGreaterThan(0);
|
||
|
|
expectIndexes(rooms, ['status_createdAt_id']);
|
||
|
|
expect(rooms.totalDocsExamined).toBe(activeRooms);
|
||
|
|
expect(participants.collscan).toBe(false);
|
||
|
|
expect(participants.indexes.some((index) => ['status_kind', 'kind_status_joinedAt_id'].includes(index))).toBe(
|
||
|
|
true
|
||
|
|
);
|
||
|
|
expect(participants.totalDocsExamined).toBe(activeParticipants);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('in-flight counts read only index keys', async () => {
|
||
|
|
const count = (filter: Document) => [{ $match: filter }, { $group: { _id: 1, n: { $sum: 1 } } }];
|
||
|
|
const egresses = await aggregate(Collection.EGRESSES, count(IN_FLIGHT_EGRESS));
|
||
|
|
const ingress = await aggregate(Collection.INGRESS_SESSIONS, count(PUBLISHING_INGRESS));
|
||
|
|
|
||
|
|
expectIndexes(egresses, ['status_startedAt']);
|
||
|
|
expect(egresses.totalDocsExamined).toBe(0);
|
||
|
|
expectIndexes(ingress, ['status']);
|
||
|
|
expect(ingress.totalDocsExamined).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('recent rooms merge two sorted index branches and today reads at most 25 rollups', async () => {
|
||
|
|
const recent = await expectIndexBacked(
|
||
|
|
db,
|
||
|
|
db
|
||
|
|
.collection(Collection.ROOMS)
|
||
|
|
.find({ status: { $in: ['active', 'closed'] } }, { projection: ROOM_LIST_PROJECTION })
|
||
|
|
.sort({ createdAt: -1, _id: -1 })
|
||
|
|
.limit(RECENT_ROOMS),
|
||
|
|
{ maxDocsExamined: RECENT_ROOMS }
|
||
|
|
);
|
||
|
|
const window = todayWindow(now, 'Pacific/Kiritimati');
|
||
|
|
const today = await expectIndexBacked(
|
||
|
|
db,
|
||
|
|
db
|
||
|
|
.collection(Collection.ANALYTICS_HOURLY)
|
||
|
|
.find({ scope: 'global', bucket: { $gte: window.from, $lt: now } })
|
||
|
|
.sort({ bucket: 1 }),
|
||
|
|
{ maxDocsExamined: 25 }
|
||
|
|
);
|
||
|
|
|
||
|
|
expectIndexes(recent, ['status_createdAt_id']);
|
||
|
|
expect(recent.planStages).not.toContain('SORT');
|
||
|
|
expect(recent.totalKeysExamined).toBeLessThanOrEqual(2 * RECENT_ROOMS);
|
||
|
|
expectIndexes(today, ['scope_bucket']);
|
||
|
|
expect(today.nReturned).toBeLessThanOrEqual(25);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('reports the p95 of every budgeted query and fails above its budget', async () => {
|
||
|
|
const repository = new OverviewRepository(db);
|
||
|
|
const rows: [string, number, () => Promise<unknown>][] = [
|
||
|
|
['Full overview (PRO, with nodes)', 100, () => service.overview('Europe/Madrid')],
|
||
|
|
['Active rooms groups', 30, () => repository.roomGroups(threshold)],
|
||
|
|
['Active participant groups', 30, () => repository.participantGroups(threshold)]
|
||
|
|
];
|
||
|
|
|
||
|
|
report.push('| Query | Budget | p95 |', '| --- | --- | --- |');
|
||
|
|
|
||
|
|
const over: string[] = [];
|
||
|
|
|
||
|
|
for (const [name, budget, run] of rows) {
|
||
|
|
const measured = await p95(run);
|
||
|
|
|
||
|
|
report.push(`| ${name} | ${String(budget)} ms | ${measured.toFixed(1)} ms |`);
|
||
|
|
|
||
|
|
if (measured > budget) {
|
||
|
|
over.push(`${name}: ${measured.toFixed(1)} ms > ${String(budget)} ms`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
expectWithinBudgets(over);
|
||
|
|
});
|
||
|
|
});
|