Files
Profa b91627f282 fix(quality): implement GOAP Issue #149 code quality remediation
Phase 1: Test Determinism
- Create SeededRandom utility with Mulberry32 PRNG (src/utils/SeededRandom.ts)
- Migrate 258+ Math.random() calls to SeededRandom across 65+ test files
- Add timer test utilities (tests/helpers/timerTestUtils.ts)
- Add jest.useFakeTimers() pattern to 13 test files

Phase 2: Type Safety (63.5% reduction)
- Reduce `any` types from ~1,800 to 656
- Add 50+ new interfaces across agents, core, mcp, cli modules
- Fix all TypeScript errors (0 errors, build passes)
- Replace Record<string, any> with Record<string, unknown>
- Add proper type guards for hook methods (PostTaskData, TaskErrorData)

Key improvements:
- agents/: 105 → 0 any types (100% reduction)
- mcp/: 226 → 102 any types (55% reduction)
- cli/: 277 → 168 any types (39% reduction)
- core/: 199 → 175 any types (12% reduction)

Closes #149

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 17:08:12 +00:00

509 lines
15 KiB
TypeScript

/**
* Performance Benchmark Tests for Phase 3 Visualization
* Tests dashboard load times, WebSocket latency, and rendering performance
*/
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import { performance } from 'perf_hooks';
import { createSeededRandom } from '../../../src/utils/SeededRandom';
// Performance metrics collector
class PerformanceMetrics {
private measurements: Map<string, number[]> = new Map();
record(metric: string, value: number): void {
if (!this.measurements.has(metric)) {
this.measurements.set(metric, []);
}
this.measurements.get(metric)!.push(value);
}
getStats(metric: string): { min: number; max: number; avg: number; p50: number; p95: number; p99: number } {
const values = this.measurements.get(metric) || [];
if (values.length === 0) {
return { min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0 };
}
const sorted = [...values].sort((a, b) => a - b);
const sum = values.reduce((a, b) => a + b, 0);
return {
min: Math.min(...values),
max: Math.max(...values),
avg: sum / values.length,
p50: sorted[Math.floor(sorted.length * 0.50)],
p95: sorted[Math.floor(sorted.length * 0.95)],
p99: sorted[Math.floor(sorted.length * 0.99)]
};
}
clear(): void {
this.measurements.clear();
}
getAllMetrics(): Record<string, ReturnType<typeof this.getStats>> {
const result: Record<string, ReturnType<typeof this.getStats>> = {};
for (const metric of this.measurements.keys()) {
result[metric] = this.getStats(metric);
}
return result;
}
}
// Mock components for performance testing
class MockVisualizationEngine {
private rng = createSeededRandom(500001);
renderGraph(nodeCount: number): number {
const start = performance.now();
// Simulate graph layout calculation
const nodes = Array(nodeCount).fill(null).map((_, i) => ({
id: `node-${i}`,
x: this.rng.random() * 1000,
y: this.rng.random() * 600
}));
// Simulate force-directed layout iterations
for (let iteration = 0; iteration < 10; iteration++) {
nodes.forEach((node, i) => {
// Simulate force calculations
let fx = 0, fy = 0;
nodes.forEach((other, j) => {
if (i !== j) {
const dx = other.x - node.x;
const dy = other.y - node.y;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
fx += dx / distance;
fy += dy / distance;
}
});
node.x += fx * 0.01;
node.y += fy * 0.01;
});
}
return performance.now() - start;
}
async fetchDashboardData(): Promise<number> {
const start = performance.now();
// Simulate API calls
await Promise.all([
this.mockAPICall(100), // Events
this.mockAPICall(50), // Metrics
this.mockAPICall(30) // Reasoning chains
]);
return performance.now() - start;
}
private async mockAPICall(responseSize: number): Promise<void> {
return new Promise(resolve => {
const delay = this.rng.random() * 50 + responseSize * 0.5;
setTimeout(resolve, delay);
});
}
measureWebSocketLatency(messageCount: number): Promise<number[]> {
return new Promise(resolve => {
const latencies: number[] = [];
for (let i = 0; i < messageCount; i++) {
const sent = performance.now();
// Simulate network round-trip
setTimeout(() => {
const latency = performance.now() - sent;
latencies.push(latency);
if (latencies.length === messageCount) {
resolve(latencies);
}
}, this.rng.random() * 100 + 10);
}
});
}
}
describe('Performance Benchmark Tests', () => {
let metrics: PerformanceMetrics;
let engine: MockVisualizationEngine;
beforeEach(() => {
metrics = new PerformanceMetrics();
engine = new MockVisualizationEngine();
});
describe('Dashboard Load Performance', () => {
it('should load dashboard in under 2 seconds (SUCCESS CRITERION)', async () => {
const iterations = 10;
for (let i = 0; i < iterations; i++) {
const loadTime = await engine.fetchDashboardData();
metrics.record('dashboard_load', loadTime);
}
const stats = metrics.getStats('dashboard_load');
expect(stats.avg).toBeLessThan(2000);
expect(stats.p95).toBeLessThan(2000);
expect(stats.p99).toBeLessThan(2500);
console.log('Dashboard Load Stats (ms):', {
avg: stats.avg.toFixed(2),
p50: stats.p50.toFixed(2),
p95: stats.p95.toFixed(2),
p99: stats.p99.toFixed(2)
});
});
it('should handle concurrent dashboard loads', async () => {
const concurrentLoads = 5;
const start = performance.now();
const loads = Array(concurrentLoads).fill(null).map(() =>
engine.fetchDashboardData()
);
const loadTimes = await Promise.all(loads);
const totalTime = performance.now() - start;
loadTimes.forEach(time => {
metrics.record('concurrent_load', time);
});
const stats = metrics.getStats('concurrent_load');
expect(totalTime).toBeLessThan(3000); // All loads in 3s
expect(stats.avg).toBeLessThan(2000); // Each load <2s
console.log('Concurrent Load Stats (ms):', {
totalTime: totalTime.toFixed(2),
avgPerLoad: stats.avg.toFixed(2)
});
});
it('should maintain performance under repeated loads', async () => {
const loadCount = 20;
for (let i = 0; i < loadCount; i++) {
const loadTime = await engine.fetchDashboardData();
metrics.record('repeated_load', loadTime);
}
const stats = metrics.getStats('repeated_load');
// Performance should not degrade
const firstHalf = metrics.getStats('repeated_load');
expect(stats.avg).toBeLessThan(2000);
console.log('Repeated Load Stats (ms):', {
loads: loadCount,
avg: stats.avg.toFixed(2),
min: stats.min.toFixed(2),
max: stats.max.toFixed(2)
});
});
});
describe('WebSocket Latency', () => {
it('should achieve <500ms WebSocket latency (SUCCESS CRITERION)', async () => {
const messageCount = 50;
const latencies = await engine.measureWebSocketLatency(messageCount);
latencies.forEach(latency => {
metrics.record('ws_latency', latency);
});
const stats = metrics.getStats('ws_latency');
expect(stats.avg).toBeLessThan(500);
expect(stats.p95).toBeLessThan(500);
expect(stats.p99).toBeLessThan(600);
console.log('WebSocket Latency Stats (ms):', {
avg: stats.avg.toFixed(2),
p50: stats.p50.toFixed(2),
p95: stats.p95.toFixed(2),
p99: stats.p99.toFixed(2),
min: stats.min.toFixed(2),
max: stats.max.toFixed(2)
});
});
it('should maintain low latency under load', async () => {
const batches = 5;
const messagesPerBatch = 20;
for (let batch = 0; batch < batches; batch++) {
const latencies = await engine.measureWebSocketLatency(messagesPerBatch);
latencies.forEach(l => metrics.record('ws_latency_load', l));
}
const stats = metrics.getStats('ws_latency_load');
expect(stats.avg).toBeLessThan(500);
expect(stats.p95).toBeLessThan(600);
console.log('WebSocket Latency Under Load (ms):', {
batches,
messagesPerBatch,
avg: stats.avg.toFixed(2),
p95: stats.p95.toFixed(2)
});
});
it('should measure latency variance', async () => {
const messageCount = 100;
const latencies = await engine.measureWebSocketLatency(messageCount);
latencies.forEach(l => metrics.record('ws_latency_variance', l));
const stats = metrics.getStats('ws_latency_variance');
const variance = latencies.reduce((sum, l) => {
return sum + Math.pow(l - stats.avg, 2);
}, 0) / latencies.length;
const stdDev = Math.sqrt(variance);
expect(stdDev).toBeLessThan(200); // Consistent latency
console.log('WebSocket Latency Variance:', {
avg: stats.avg.toFixed(2),
stdDev: stdDev.toFixed(2),
cv: (stdDev / stats.avg * 100).toFixed(2) + '%' // Coefficient of variation
});
});
});
describe('Mind Map Rendering Performance', () => {
it('should render 100 nodes in <100ms (SUCCESS CRITERION)', () => {
const iterations = 20;
const nodeCount = 100;
for (let i = 0; i < iterations; i++) {
const renderTime = engine.renderGraph(nodeCount);
metrics.record('render_100_nodes', renderTime);
}
const stats = metrics.getStats('render_100_nodes');
expect(stats.avg).toBeLessThan(100);
expect(stats.p95).toBeLessThan(150);
expect(stats.p99).toBeLessThan(200);
console.log('Render 100 Nodes Stats (ms):', {
avg: stats.avg.toFixed(2),
p50: stats.p50.toFixed(2),
p95: stats.p95.toFixed(2),
p99: stats.p99.toFixed(2)
});
});
it('should render 500 nodes efficiently', () => {
const iterations = 10;
const nodeCount = 500;
for (let i = 0; i < iterations; i++) {
const renderTime = engine.renderGraph(nodeCount);
metrics.record('render_500_nodes', renderTime);
}
const stats = metrics.getStats('render_500_nodes');
expect(stats.avg).toBeLessThan(500);
expect(stats.p95).toBeLessThan(700);
console.log('Render 500 Nodes Stats (ms):', {
avg: stats.avg.toFixed(2),
p95: stats.p95.toFixed(2)
});
});
it('should render 1000 nodes within reasonable time', () => {
const iterations = 5;
const nodeCount = 1000;
for (let i = 0; i < iterations; i++) {
const renderTime = engine.renderGraph(nodeCount);
metrics.record('render_1000_nodes', renderTime);
}
const stats = metrics.getStats('render_1000_nodes');
expect(stats.avg).toBeLessThan(1000);
expect(stats.p95).toBeLessThan(1500);
console.log('Render 1000 Nodes Stats (ms):', {
avg: stats.avg.toFixed(2),
p95: stats.p95.toFixed(2)
});
});
it('should scale linearly with node count', () => {
const nodeCounts = [50, 100, 200, 400];
const scalingData: Array<{ nodes: number; time: number }> = [];
nodeCounts.forEach(count => {
const renderTime = engine.renderGraph(count);
scalingData.push({ nodes: count, time: renderTime });
metrics.record(`render_${count}_nodes`, renderTime);
});
// Check if scaling is approximately linear
const timePerNode = scalingData.map(d => d.time / d.nodes);
const avgTimePerNode = timePerNode.reduce((a, b) => a + b) / timePerNode.length;
// Variance should be low for linear scaling
const variance = timePerNode.reduce((sum, t) => {
return sum + Math.pow(t - avgTimePerNode, 2);
}, 0) / timePerNode.length;
console.log('Scaling Analysis:', {
nodeCounts,
avgTimePerNode: avgTimePerNode.toFixed(4),
variance: variance.toFixed(6),
scalingData: scalingData.map(d => ({
nodes: d.nodes,
time: d.time.toFixed(2),
timePerNode: (d.time / d.nodes).toFixed(4)
}))
});
expect(variance).toBeLessThan(0.001); // Low variance indicates linear scaling
});
});
describe('Memory Performance', () => {
it('should not leak memory during repeated operations', () => {
const initialMemory = process.memoryUsage().heapUsed;
const iterations = 100;
for (let i = 0; i < iterations; i++) {
engine.renderGraph(100);
}
// Force garbage collection if available
if (global.gc) {
global.gc();
}
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = finalMemory - initialMemory;
const increaseInMB = memoryIncrease / 1024 / 1024;
console.log('Memory Usage:', {
initial: (initialMemory / 1024 / 1024).toFixed(2) + ' MB',
final: (finalMemory / 1024 / 1024).toFixed(2) + ' MB',
increase: increaseInMB.toFixed(2) + ' MB'
});
expect(increaseInMB).toBeLessThan(50); // Less than 50MB increase
});
it('should handle large dataset efficiently', () => {
const nodeCount = 5000;
const memoryBefore = process.memoryUsage().heapUsed;
const renderTime = engine.renderGraph(nodeCount);
const memoryAfter = process.memoryUsage().heapUsed;
const memoryUsed = (memoryAfter - memoryBefore) / 1024 / 1024;
console.log('Large Dataset Performance:', {
nodes: nodeCount,
renderTime: renderTime.toFixed(2) + ' ms',
memoryUsed: memoryUsed.toFixed(2) + ' MB',
memoryPerNode: (memoryUsed / nodeCount * 1024).toFixed(2) + ' KB'
});
expect(renderTime).toBeLessThan(5000); // 5 seconds for 5000 nodes
expect(memoryUsed).toBeLessThan(200); // Less than 200MB
});
});
describe('Throughput Benchmarks', () => {
it('should process high event throughput', async () => {
const eventCount = 1000;
const startTime = performance.now();
// Simulate processing events
const events = Array(eventCount).fill(null).map((_, i) => ({
id: `evt-${i}`,
timestamp: Date.now(),
type: 'test'
}));
// Process in batches
const batchSize = 100;
for (let i = 0; i < events.length; i += batchSize) {
const batch = events.slice(i, i + batchSize);
await new Promise(resolve => setTimeout(resolve, 10)); // Simulate processing
}
const duration = performance.now() - startTime;
const throughput = (eventCount / duration) * 1000; // Events per second
metrics.record('event_throughput', throughput);
console.log('Event Throughput:', {
events: eventCount,
duration: duration.toFixed(2) + ' ms',
throughput: throughput.toFixed(0) + ' events/sec'
});
expect(throughput).toBeGreaterThan(500); // At least 500 events/sec
});
it('should handle burst traffic', async () => {
const burstSize = 500;
const startTime = performance.now();
const rng = createSeededRandom(21001);
// Simulate burst of events
await Promise.all(
Array(burstSize).fill(null).map(() =>
new Promise(resolve => setTimeout(resolve, rng.random() * 10))
)
);
const duration = performance.now() - startTime;
console.log('Burst Traffic:', {
burstSize,
duration: duration.toFixed(2) + ' ms',
avgTimePerEvent: (duration / burstSize).toFixed(2) + ' ms'
});
expect(duration).toBeLessThan(1000); // Handle burst in <1s
});
});
describe('Performance Summary', () => {
it('should generate comprehensive performance report', () => {
const allMetrics = metrics.getAllMetrics();
console.log('\n=== PERFORMANCE BENCHMARK SUMMARY ===\n');
Object.entries(allMetrics).forEach(([metric, stats]) => {
console.log(`${metric}:`, {
avg: stats.avg.toFixed(2),
p50: stats.p50.toFixed(2),
p95: stats.p95.toFixed(2),
p99: stats.p99.toFixed(2),
min: stats.min.toFixed(2),
max: stats.max.toFixed(2)
});
});
console.log('\n=== SUCCESS CRITERIA ===');
console.log('✓ Dashboard load time: <2s');
console.log('✓ WebSocket latency: <500ms');
console.log('✓ Mind map render (100 nodes): <100ms');
expect(Object.keys(allMetrics).length).toBeGreaterThan(0);
});
});
});