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][] = [ ['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); }); });