import { describe, it, expect, beforeAll, afterAll, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { ConfigService } from "@nestjs/config"; import { JobEventsService } from "./job-events.service"; import { PrismaService } from "../prisma/prisma.service"; import { VaultService } from "../vault/vault.service"; import { CryptoService } from "../federation/crypto.service"; import { JOB_CREATED, JOB_STARTED, STEP_STARTED } from "./event-types"; /** * Performance tests for JobEventsService * * These tests verify that the composite index [jobId, timestamp] improves * query performance for the most common access patterns. * * NOTE: These tests require a real database connection with realistic data volume. * Run with: pnpm test:api -- job-events.performance.spec.ts */ const shouldRunDbIntegrationTests = process.env.RUN_DB_TESTS === "true" && Boolean(process.env.DATABASE_URL); const describeFn = shouldRunDbIntegrationTests ? describe : describe.skip; describeFn("JobEventsService Performance", () => { let service: JobEventsService; let prisma: PrismaService; let testJobId: string; let testWorkspaceId: string; beforeAll(async () => { // Mock ConfigService for VaultService/CryptoService const mockConfigService = { get: vi.fn((key: string) => { const config: Record = { ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", OPENBAO_ADDR: "http://localhost:8200", OPENBAO_ROLE_ID: "test-role-id", OPENBAO_SECRET_ID: "test-secret-id", }; return config[key] || null; }), }; const module: TestingModule = await Test.createTestingModule({ providers: [ JobEventsService, PrismaService, { provide: ConfigService, useValue: mockConfigService, }, VaultService, CryptoService, ], }).compile(); service = module.get(JobEventsService); prisma = module.get(PrismaService); // Create test workspace const workspace = await prisma.workspace.create({ data: { name: "Performance Test Workspace", owner: { create: { email: `perf-test-${Date.now()}@example.com`, name: "Performance Test User", }, }, }, }); testWorkspaceId = workspace.id; // Create test job with many events const job = await prisma.runnerJob.create({ data: { workspaceId: testWorkspaceId, type: "code-task", status: "RUNNING", priority: 5, progressPercent: 0, }, }); testJobId = job.id; // Create 1000 events to simulate realistic load const events = []; for (let i = 0; i < 1000; i++) { events.push({ jobId: testJobId, type: i % 3 === 0 ? JOB_STARTED : i % 3 === 1 ? STEP_STARTED : JOB_CREATED, timestamp: new Date(Date.now() - (1000 - i) * 1000), // Events over ~16 minutes actor: "system", payload: { iteration: i }, }); } // Batch insert for performance await prisma.jobEvent.createMany({ data: events, }); }); afterAll(async () => { // Clean up test data await prisma.jobEvent.deleteMany({ where: { jobId: testJobId }, }); await prisma.runnerJob.delete({ where: { id: testJobId }, }); await prisma.workspace.delete({ where: { id: testWorkspaceId }, }); await prisma.$disconnect(); }); describe("Query Performance", () => { it("should efficiently query events by jobId with timestamp ordering", async () => { const startTime = performance.now(); const result = await service.getEventsByJobId(testJobId, { page: 1, limit: 50, }); const endTime = performance.now(); const queryTime = endTime - startTime; expect(result.data).toHaveLength(50); expect(result.meta.total).toBe(1000); expect(queryTime).toBeLessThan(100); // Should complete in under 100ms // Verify events are ordered by timestamp ascending for (let i = 1; i < result.data.length; i++) { expect(result.data[i].timestamp.getTime()).toBeGreaterThanOrEqual( result.data[i - 1].timestamp.getTime() ); } }); it("should efficiently query events by jobId and type with timestamp ordering", async () => { const startTime = performance.now(); const result = await service.getEventsByJobId(testJobId, { type: JOB_STARTED, page: 1, limit: 50, }); const endTime = performance.now(); const queryTime = endTime - startTime; expect(result.data.length).toBeGreaterThan(0); expect(result.data.every((e) => e.type === JOB_STARTED)).toBe(true); expect(queryTime).toBeLessThan(100); // Should complete in under 100ms }); it("should efficiently query events with timestamp range (streaming pattern)", async () => { // Get a timestamp from the middle of our test data const midpointTime = new Date(Date.now() - 500 * 1000); const startTime = performance.now(); const events = await prisma.jobEvent.findMany({ where: { jobId: testJobId, timestamp: { gt: midpointTime }, }, orderBy: { timestamp: "asc" }, take: 100, }); const endTime = performance.now(); const queryTime = endTime - startTime; expect(events.length).toBeGreaterThan(0); expect(events.length).toBeLessThanOrEqual(100); expect(queryTime).toBeLessThan(50); // Range queries should be very fast with index // Verify all events are after the midpoint events.forEach((event) => { expect(event.timestamp.getTime()).toBeGreaterThan(midpointTime.getTime()); }); }); it("should use the composite index in query plan", async () => { // Execute EXPLAIN ANALYZE to verify index usage const explainResult = await prisma.$queryRaw>` EXPLAIN (FORMAT JSON) SELECT * FROM job_events WHERE job_id = ${testJobId}::uuid ORDER BY timestamp ASC LIMIT 50 `; const queryPlan = JSON.stringify(explainResult); // Verify that an index scan is used (not a sequential scan) expect(queryPlan.toLowerCase()).toContain("index"); expect(queryPlan.toLowerCase()).not.toContain("seq scan on job_events"); // The composite index should be named something like: // job_events_job_id_timestamp_idx or similar expect(queryPlan.includes("job_events_job_id") || queryPlan.includes("index")).toBe(true); }); }); describe("Pagination Performance", () => { it("should efficiently paginate through all events", async () => { const startTime = performance.now(); // Fetch page 10 (events 450-499) const result = await service.getEventsByJobId(testJobId, { page: 10, limit: 50, }); const endTime = performance.now(); const queryTime = endTime - startTime; expect(result.data).toHaveLength(50); expect(result.meta.page).toBe(10); expect(queryTime).toBeLessThan(150); // Should complete in under 150ms even with OFFSET }); }); describe("Concurrent Query Performance", () => { it("should handle multiple concurrent queries efficiently", async () => { const startTime = performance.now(); // Simulate 10 concurrent clients querying the same job const queries = Array.from({ length: 10 }, (_, i) => service.getEventsByJobId(testJobId, { page: i + 1, limit: 50, }) ); const results = await Promise.all(queries); const endTime = performance.now(); const totalTime = endTime - startTime; expect(results).toHaveLength(10); results.forEach((result, i) => { expect(result.data).toHaveLength(50); expect(result.meta.page).toBe(i + 1); }); // All 10 queries should complete in under 500ms total expect(totalTime).toBeLessThan(500); }); }); });