mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
b91627f282
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>
509 lines
15 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|