/** * Performance Test: Agent Spawner Throughput * * Benchmarks the spawner service under concurrent load to verify * it meets performance requirements for agent orchestration. * * Covers issue #229 (ORCH-128) */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { AgentSpawnerService } from "../../src/spawner/agent-spawner.service"; import { AgentsController } from "../../src/api/agents/agents.controller"; import { QueueService } from "../../src/queue/queue.service"; import { AgentLifecycleService } from "../../src/spawner/agent-lifecycle.service"; import { KillswitchService } from "../../src/killswitch/killswitch.service"; import { ConfigService } from "@nestjs/config"; function createSpawnRequest(taskId: string): { taskId: string; agentType: string; context: { repository: string; branch: string; workItems: string[] }; } { return { taskId, agentType: "worker", context: { repository: "https://git.example.com/repo.git", branch: "main", workItems: [`US-${taskId}`], }, }; } describe("Performance: Agent Spawner Throughput", () => { let controller: AgentsController; let spawnerService: AgentSpawnerService; const mockConfigService = { get: vi.fn((key: string, defaultValue?: unknown) => { const config: Record = { "orchestrator.claude.apiKey": "test-api-key", }; return config[key] ?? defaultValue; }), }; beforeEach(() => { vi.clearAllMocks(); spawnerService = new AgentSpawnerService(mockConfigService as unknown as ConfigService); const queueService = { addTask: vi.fn().mockResolvedValue(undefined), } as unknown as QueueService; const lifecycleService = { getAgentLifecycleState: vi.fn(), } as unknown as AgentLifecycleService; const killswitchService = { killAgent: vi.fn(), killAllAgents: vi.fn(), } as unknown as KillswitchService; controller = new AgentsController( queueService, spawnerService, lifecycleService, killswitchService ); }); describe("Spawn latency", () => { it("should spawn a single agent in under 50ms", async () => { // Warmup await controller.spawn(createSpawnRequest("warmup-1")); const start = performance.now(); await controller.spawn(createSpawnRequest("perf-single-001")); const duration = performance.now() - start; expect(duration).toBeLessThan(50); }); it("should spawn 100 agents sequentially in under 500ms", async () => { // Warmup for (let i = 0; i < 5; i++) { await controller.spawn(createSpawnRequest(`warmup-seq-${String(i)}`)); } const start = performance.now(); for (let i = 0; i < 100; i++) { await controller.spawn(createSpawnRequest(`perf-seq-${String(i)}`)); } const duration = performance.now() - start; expect(duration).toBeLessThan(500); // 100 sequential + 5 warmup const agents = spawnerService.listAgentSessions(); expect(agents.length).toBeGreaterThanOrEqual(100); }); it("should spawn 100 agents concurrently in under 200ms", async () => { // Warmup for (let i = 0; i < 5; i++) { await controller.spawn(createSpawnRequest(`warmup-conc-${String(i)}`)); } const start = performance.now(); const promises = Array.from({ length: 100 }, (_, i) => controller.spawn(createSpawnRequest(`perf-concurrent-${String(i)}`)) ); const results = await Promise.all(promises); const duration = performance.now() - start; expect(duration).toBeLessThan(200); expect(results).toHaveLength(100); // Verify all IDs are unique const ids = new Set(results.map((r) => r.agentId)); expect(ids.size).toBe(100); }); }); describe("Session lookup performance", () => { it("should look up agents by ID in under 10ms with 1000 sessions", async () => { // Pre-populate 1000 sessions const agentIds: string[] = []; for (let i = 0; i < 1000; i++) { const result = await controller.spawn(createSpawnRequest(`perf-lookup-${String(i)}`)); agentIds.push(result.agentId); } // Measure lookup time for random agents const lookupStart = performance.now(); for (let i = 0; i < 100; i++) { const randomIdx = Math.floor(Math.random() * agentIds.length); const session = spawnerService.getAgentSession(agentIds[randomIdx] ?? ""); expect(session).toBeDefined(); } const lookupDuration = performance.now() - lookupStart; // 100 lookups should complete in under 10ms expect(lookupDuration).toBeLessThan(10); }); it("should list all sessions in under 5ms with 1000 sessions", async () => { // Pre-populate 1000 sessions for (let i = 0; i < 1000; i++) { await controller.spawn(createSpawnRequest(`perf-list-${String(i)}`)); } const listStart = performance.now(); const sessions = spawnerService.listAgentSessions(); const listDuration = performance.now() - listStart; expect(sessions).toHaveLength(1000); expect(listDuration).toBeLessThan(5); }); }); describe("Memory efficiency", () => { it("should not have excessive memory growth after 1000 spawns", async () => { // Force GC if available, then settle if (global.gc) global.gc(); const memBefore = process.memoryUsage().heapUsed; for (let i = 0; i < 1000; i++) { await controller.spawn(createSpawnRequest(`perf-mem-${String(i)}`)); } const memAfter = process.memoryUsage().heapUsed; const memGrowthMB = (memAfter - memBefore) / 1024 / 1024; // 1000 agent sessions should use less than 50MB expect(memGrowthMB).toBeLessThan(50); }); }); });