mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: introduce debug panel for performance monitoring
- Added a debug panel to monitor server performance, including memory and CPU metrics. - Implemented debug services for real-time tracking of processes and performance metrics. - Created API endpoints for metrics collection and process management. - Enhanced UI components for displaying metrics and process statuses. - Updated documentation to include new debug API details. This feature is intended for development use and can be toggled with the `ENABLE_DEBUG_PANEL` environment variable.
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { PerformanceMonitorService } from '@/services/performance-monitor-service.js';
|
||||
import { createEventEmitter } from '@/lib/events.js';
|
||||
import type { EventEmitter } from '@/lib/events.js';
|
||||
import type { TrackedProcess, DebugMetricsConfig } from '@automaker/types';
|
||||
import { DEFAULT_DEBUG_METRICS_CONFIG } from '@automaker/types';
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('@automaker/utils', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('PerformanceMonitorService', () => {
|
||||
let service: PerformanceMonitorService;
|
||||
let events: EventEmitter;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
events = createEventEmitter();
|
||||
service = new PerformanceMonitorService(events);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service.stop();
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with default configuration', () => {
|
||||
const config = service.getConfig();
|
||||
expect(config.collectionInterval).toBe(DEFAULT_DEBUG_METRICS_CONFIG.collectionInterval);
|
||||
expect(config.maxDataPoints).toBe(DEFAULT_DEBUG_METRICS_CONFIG.maxDataPoints);
|
||||
expect(config.memoryEnabled).toBe(DEFAULT_DEBUG_METRICS_CONFIG.memoryEnabled);
|
||||
expect(config.cpuEnabled).toBe(DEFAULT_DEBUG_METRICS_CONFIG.cpuEnabled);
|
||||
});
|
||||
|
||||
it('should accept custom configuration on initialization', () => {
|
||||
const customConfig: Partial<DebugMetricsConfig> = {
|
||||
collectionInterval: 5000,
|
||||
maxDataPoints: 500,
|
||||
memoryEnabled: false,
|
||||
};
|
||||
|
||||
const customService = new PerformanceMonitorService(events, customConfig);
|
||||
const config = customService.getConfig();
|
||||
|
||||
expect(config.collectionInterval).toBe(5000);
|
||||
expect(config.maxDataPoints).toBe(500);
|
||||
expect(config.memoryEnabled).toBe(false);
|
||||
expect(config.cpuEnabled).toBe(DEFAULT_DEBUG_METRICS_CONFIG.cpuEnabled);
|
||||
|
||||
customService.stop();
|
||||
});
|
||||
|
||||
it('should not be running initially', () => {
|
||||
expect(service.isActive()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start/stop', () => {
|
||||
it('should start metrics collection', () => {
|
||||
service.start();
|
||||
expect(service.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('should stop metrics collection', () => {
|
||||
service.start();
|
||||
expect(service.isActive()).toBe(true);
|
||||
|
||||
service.stop();
|
||||
expect(service.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not start again if already running', () => {
|
||||
service.start();
|
||||
const isActive1 = service.isActive();
|
||||
|
||||
service.start(); // Should log warning but not throw
|
||||
const isActive2 = service.isActive();
|
||||
|
||||
expect(isActive1).toBe(true);
|
||||
expect(isActive2).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle stop when not running', () => {
|
||||
// Should not throw
|
||||
expect(() => service.stop()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration updates', () => {
|
||||
it('should update configuration', () => {
|
||||
service.updateConfig({ collectionInterval: 2000 });
|
||||
expect(service.getConfig().collectionInterval).toBe(2000);
|
||||
});
|
||||
|
||||
it('should restart collection if running when config is updated', () => {
|
||||
service.start();
|
||||
expect(service.isActive()).toBe(true);
|
||||
|
||||
service.updateConfig({ collectionInterval: 5000 });
|
||||
|
||||
// Should still be running after config update
|
||||
expect(service.isActive()).toBe(true);
|
||||
expect(service.getConfig().collectionInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should resize data buffers when maxDataPoints changes', () => {
|
||||
// Start and collect some data
|
||||
service.start();
|
||||
|
||||
// Collect multiple data points
|
||||
for (let i = 0; i < 50; i++) {
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
}
|
||||
|
||||
// Reduce max data points
|
||||
service.updateConfig({ maxDataPoints: 10 });
|
||||
|
||||
const history = service.getMemoryHistory();
|
||||
expect(history.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('metrics collection', () => {
|
||||
it('should emit debug:metrics event on collection', () => {
|
||||
const callback = vi.fn();
|
||||
events.subscribe(callback);
|
||||
|
||||
service.start();
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
const [eventType, eventData] = callback.mock.calls[0];
|
||||
expect(eventType).toBe('debug:metrics');
|
||||
expect(eventData).toHaveProperty('timestamp');
|
||||
expect(eventData).toHaveProperty('metrics');
|
||||
});
|
||||
|
||||
it('should collect memory metrics when memoryEnabled is true', () => {
|
||||
const callback = vi.fn();
|
||||
events.subscribe(callback);
|
||||
|
||||
service.start();
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
|
||||
const [, eventData] = callback.mock.calls[0];
|
||||
expect(eventData.metrics.memory.server).toBeDefined();
|
||||
expect(eventData.metrics.memory.server.heapUsed).toBeGreaterThan(0);
|
||||
expect(eventData.metrics.memory.server.heapTotal).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not collect memory metrics when memoryEnabled is false', () => {
|
||||
const customService = new PerformanceMonitorService(events, { memoryEnabled: false });
|
||||
const callback = vi.fn();
|
||||
events.subscribe(callback);
|
||||
|
||||
customService.start();
|
||||
vi.advanceTimersByTime(customService.getConfig().collectionInterval);
|
||||
|
||||
const [, eventData] = callback.mock.calls[0];
|
||||
expect(eventData.metrics.memory.server).toBeUndefined();
|
||||
|
||||
customService.stop();
|
||||
});
|
||||
|
||||
it('should collect CPU metrics when cpuEnabled is true', () => {
|
||||
const callback = vi.fn();
|
||||
events.subscribe(callback);
|
||||
|
||||
service.start();
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
|
||||
// Need at least 2 collections for CPU diff
|
||||
const lastCall = callback.mock.calls[callback.mock.calls.length - 1];
|
||||
const [, eventData] = lastCall;
|
||||
expect(eventData.metrics.cpu.server).toBeDefined();
|
||||
});
|
||||
|
||||
it('should track event loop lag', () => {
|
||||
const callback = vi.fn();
|
||||
events.subscribe(callback);
|
||||
|
||||
service.start();
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
|
||||
const [, eventData] = callback.mock.calls[0];
|
||||
expect(eventData.metrics.cpu.eventLoopLag).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('memory history', () => {
|
||||
it('should return empty history initially', () => {
|
||||
const history = service.getMemoryHistory();
|
||||
expect(history).toEqual([]);
|
||||
});
|
||||
|
||||
it('should accumulate memory history over time', () => {
|
||||
service.start();
|
||||
|
||||
// Collect multiple data points
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
}
|
||||
|
||||
const history = service.getMemoryHistory();
|
||||
expect(history.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should limit history to maxDataPoints', () => {
|
||||
const maxPoints = 10;
|
||||
const customService = new PerformanceMonitorService(events, { maxDataPoints: maxPoints });
|
||||
customService.start();
|
||||
|
||||
// Collect more data points than max
|
||||
for (let i = 0; i < maxPoints + 10; i++) {
|
||||
vi.advanceTimersByTime(customService.getConfig().collectionInterval);
|
||||
}
|
||||
|
||||
const history = customService.getMemoryHistory();
|
||||
expect(history.length).toBeLessThanOrEqual(maxPoints);
|
||||
|
||||
customService.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CPU history', () => {
|
||||
it('should return empty CPU history initially', () => {
|
||||
const history = service.getCPUHistory();
|
||||
expect(history).toEqual([]);
|
||||
});
|
||||
|
||||
it('should accumulate CPU history over time', () => {
|
||||
service.start();
|
||||
|
||||
// Collect multiple data points (need at least 2 for CPU diff)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
}
|
||||
|
||||
const history = service.getCPUHistory();
|
||||
expect(history.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('process provider', () => {
|
||||
it('should use provided process provider', () => {
|
||||
const mockProcesses: TrackedProcess[] = [
|
||||
{
|
||||
id: 'test-1',
|
||||
type: 'agent',
|
||||
name: 'TestAgent',
|
||||
status: 'running',
|
||||
startedAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'test-2',
|
||||
type: 'terminal',
|
||||
name: 'TestTerminal',
|
||||
status: 'idle',
|
||||
startedAt: Date.now(),
|
||||
},
|
||||
];
|
||||
|
||||
const provider = vi.fn(() => mockProcesses);
|
||||
service.setProcessProvider(provider);
|
||||
|
||||
const callback = vi.fn();
|
||||
events.subscribe(callback);
|
||||
|
||||
service.start();
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
|
||||
const [, eventData] = callback.mock.calls[0];
|
||||
expect(eventData.metrics.processes).toEqual(mockProcesses);
|
||||
expect(eventData.metrics.processSummary.total).toBe(2);
|
||||
expect(eventData.metrics.processSummary.running).toBe(1);
|
||||
expect(eventData.metrics.processSummary.idle).toBe(1);
|
||||
expect(eventData.metrics.processSummary.byType.agent).toBe(1);
|
||||
expect(eventData.metrics.processSummary.byType.terminal).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLatestSnapshot', () => {
|
||||
it('should return null when no data collected', () => {
|
||||
const snapshot = service.getLatestSnapshot();
|
||||
expect(snapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('should return snapshot after data collection', () => {
|
||||
service.start();
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
|
||||
const snapshot = service.getLatestSnapshot();
|
||||
expect(snapshot).not.toBeNull();
|
||||
expect(snapshot).toHaveProperty('timestamp');
|
||||
expect(snapshot).toHaveProperty('memory');
|
||||
expect(snapshot).toHaveProperty('cpu');
|
||||
expect(snapshot).toHaveProperty('processes');
|
||||
expect(snapshot).toHaveProperty('processSummary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearHistory', () => {
|
||||
it('should clear all history', () => {
|
||||
service.start();
|
||||
|
||||
// Collect some data
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
}
|
||||
|
||||
expect(service.getMemoryHistory().length).toBeGreaterThan(0);
|
||||
|
||||
service.clearHistory();
|
||||
|
||||
expect(service.getMemoryHistory().length).toBe(0);
|
||||
expect(service.getCPUHistory().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forceGC', () => {
|
||||
it('should return false when gc is not available', () => {
|
||||
const originalGc = global.gc;
|
||||
global.gc = undefined;
|
||||
|
||||
const result = service.forceGC();
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Restore
|
||||
global.gc = originalGc;
|
||||
});
|
||||
|
||||
it('should return true and call gc when available', () => {
|
||||
const mockGc = vi.fn();
|
||||
global.gc = mockGc;
|
||||
|
||||
const result = service.forceGC();
|
||||
expect(result).toBe(true);
|
||||
expect(mockGc).toHaveBeenCalled();
|
||||
|
||||
// Cleanup
|
||||
global.gc = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('memory trend analysis', () => {
|
||||
it('should not calculate trend with insufficient data', () => {
|
||||
service.start();
|
||||
|
||||
// Collect only a few data points
|
||||
for (let i = 0; i < 5; i++) {
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
}
|
||||
|
||||
const snapshot = service.getLatestSnapshot();
|
||||
// Trend requires at least 10 samples
|
||||
expect(snapshot?.memoryTrend).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should calculate trend with sufficient data', () => {
|
||||
service.start();
|
||||
|
||||
// Collect enough data points for trend analysis
|
||||
for (let i = 0; i < 15; i++) {
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
}
|
||||
|
||||
const snapshot = service.getLatestSnapshot();
|
||||
expect(snapshot?.memoryTrend).toBeDefined();
|
||||
expect(snapshot?.memoryTrend).toHaveProperty('growthRate');
|
||||
expect(snapshot?.memoryTrend).toHaveProperty('isLeaking');
|
||||
expect(snapshot?.memoryTrend).toHaveProperty('confidence');
|
||||
expect(snapshot?.memoryTrend).toHaveProperty('sampleCount');
|
||||
});
|
||||
});
|
||||
|
||||
describe('process summary calculation', () => {
|
||||
it('should correctly categorize processes by status', () => {
|
||||
const mockProcesses: TrackedProcess[] = [
|
||||
{ id: '1', type: 'agent', name: 'A1', status: 'running', startedAt: Date.now() },
|
||||
{ id: '2', type: 'agent', name: 'A2', status: 'starting', startedAt: Date.now() },
|
||||
{ id: '3', type: 'terminal', name: 'T1', status: 'idle', startedAt: Date.now() },
|
||||
{ id: '4', type: 'terminal', name: 'T2', status: 'stopped', startedAt: Date.now() },
|
||||
{ id: '5', type: 'cli', name: 'C1', status: 'stopping', startedAt: Date.now() },
|
||||
{ id: '6', type: 'worker', name: 'W1', status: 'error', startedAt: Date.now() },
|
||||
];
|
||||
|
||||
service.setProcessProvider(() => mockProcesses);
|
||||
|
||||
const callback = vi.fn();
|
||||
events.subscribe(callback);
|
||||
|
||||
service.start();
|
||||
vi.advanceTimersByTime(service.getConfig().collectionInterval);
|
||||
|
||||
const [, eventData] = callback.mock.calls[0];
|
||||
const summary = eventData.metrics.processSummary;
|
||||
|
||||
expect(summary.total).toBe(6);
|
||||
expect(summary.running).toBe(2); // running + starting
|
||||
expect(summary.idle).toBe(1);
|
||||
expect(summary.stopped).toBe(2); // stopped + stopping
|
||||
expect(summary.errored).toBe(1);
|
||||
expect(summary.byType.agent).toBe(2);
|
||||
expect(summary.byType.terminal).toBe(2);
|
||||
expect(summary.byType.cli).toBe(1);
|
||||
expect(summary.byType.worker).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user