- Fix CRITICAL: Increase single-spawn threshold from 10ms to 50ms (CI flakiness) - Fix CRITICAL: Replace no-op validation test with real backoff scale tests - Fix IMPORTANT: Add warmup iterations before all timed measurements - Fix IMPORTANT: Increase scan position ratio tolerance to 10x for sub-ms noise - Refactored queue perf tests to use actual service methods (calculateBackoffDelay) - Helper function to reduce spawn request duplication Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
185 lines
5.7 KiB
TypeScript
185 lines
5.7 KiB
TypeScript
/**
|
|
* 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<string, unknown> = {
|
|
"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);
|
|
});
|
|
});
|
|
});
|