lightweight-browser-load-tester
Version:
A lightweight load testing tool using real browsers for streaming applications with DRM support
676 lines (542 loc) • 24.2 kB
text/typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { BrowserPool } from './managers/browser-pool';
import { TestRunner } from './controllers/test-runner';
import { TestConfiguration, BrowserPoolConfig } from './types';
import { chromium } from 'playwright';
// Mock Playwright for performance tests
vi.mock('playwright', () => ({
chromium: {
launch: vi.fn()
}
}));
describe('Performance Optimization and Resource Management', () => {
let mockBrowser: any;
let mockContext: any;
let mockPage: any;
beforeEach(() => {
// Setup comprehensive mocks for performance testing
mockPage = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
route: vi.fn().mockResolvedValue(undefined),
unroute: vi.fn().mockResolvedValue(undefined),
on: vi.fn(),
removeListener: vi.fn()
};
mockContext = {
newPage: vi.fn().mockResolvedValue(mockPage),
clearCookies: vi.fn().mockResolvedValue(undefined),
clearPermissions: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
pages: vi.fn().mockReturnValue([mockPage])
};
mockBrowser = {
newContext: vi.fn().mockResolvedValue(mockContext),
close: vi.fn().mockResolvedValue(undefined),
contexts: vi.fn().mockReturnValue([mockContext]),
on: vi.fn()
};
(chromium.launch as any).mockResolvedValue(mockBrowser);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('Memory Cleanup Between Test Runs', () => {
let browserPool: BrowserPool;
let config: BrowserPoolConfig;
beforeEach(() => {
config = {
maxInstances: 3,
minInstances: 1,
resourceLimits: {
maxMemoryPerInstance: 256,
maxCpuPercentage: 70,
maxConcurrentInstances: 5
},
browserOptions: {
headless: true,
args: ['--test-arg']
}
};
browserPool = new BrowserPool(config);
});
afterEach(async () => {
if (browserPool) {
await browserPool.shutdown();
}
});
it('should perform comprehensive memory cleanup when releasing instances', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
const instanceId = instance.id;
// Release the instance - should trigger memory cleanup
await browserPool.releaseInstance(instanceId);
// Verify cleanup operations were called
expect(mockPage.goto).toHaveBeenCalledWith('about:blank');
expect(mockContext.clearCookies).toHaveBeenCalled();
expect(mockContext.clearPermissions).toHaveBeenCalled();
expect(mockPage.evaluate).toHaveBeenCalled();
// Verify the instance is available for reuse
const poolStatus = browserPool.getPoolStatus();
expect(poolStatus.availableInstances).toBe(1);
});
it('should perform aggressive memory cleanup for instances approaching limits', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
// Mock high memory usage
instance.metrics.memoryUsage = 200; // Close to 256MB limit
// Trigger aggressive cleanup
await (browserPool as any).performAggressiveMemoryCleanup(instance);
// Verify aggressive cleanup operations
expect(mockPage.goto).toHaveBeenCalledWith('about:blank');
expect(mockContext.clearCookies).toHaveBeenCalled();
expect(mockContext.clearPermissions).toHaveBeenCalled();
expect(mockPage.evaluate).toHaveBeenCalledTimes(2); // Standard + GC
expect(mockPage.close).toHaveBeenCalled();
expect(mockContext.newPage).toHaveBeenCalled();
});
it('should reset request count during memory cleanup', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
instance.metrics.requestCount = 100;
await browserPool.releaseInstance(instance.id);
// Request count should be reset
expect(instance.metrics.requestCount).toBe(0);
});
it('should destroy instance if memory cleanup fails', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
const instanceId = instance.id;
// Make cleanup fail
mockPage.goto.mockRejectedValue(new Error('Cleanup failed'));
const destroySpy = vi.fn();
browserPool.on('instanceDestroyed', destroySpy);
await browserPool.releaseInstance(instanceId);
expect(destroySpy).toHaveBeenCalledWith({ instanceId });
});
});
describe('Configurable Resource Limits Enforcement', () => {
let browserPool: BrowserPool;
let config: BrowserPoolConfig;
beforeEach(() => {
config = {
maxInstances: 5,
minInstances: 2,
resourceLimits: {
maxMemoryPerInstance: 256,
maxCpuPercentage: 70,
maxConcurrentInstances: 5
}
};
browserPool = new BrowserPool(config);
});
afterEach(async () => {
if (browserPool) {
await browserPool.shutdown();
}
});
it('should enforce memory limits and destroy instances exceeding critical thresholds', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
await browserPool.releaseInstance(instance.id);
// Set memory usage to exceed critical threshold (1.5x limit)
instance.metrics.memoryUsage = 400; // Exceeds 256 * 1.5 = 384
const destroySpy = vi.fn();
browserPool.on('instanceDestroyedForResourceLimit', destroySpy);
// Trigger resource limit enforcement
await (browserPool as any).enforceResourceLimits();
expect(destroySpy).toHaveBeenCalledWith({ instanceId: instance.id });
});
it('should attempt cleanup before destroying instances with moderate resource usage', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
await browserPool.releaseInstance(instance.id);
// Set memory usage to exceed limit but not critical threshold
instance.metrics.memoryUsage = 300; // Exceeds 256 but less than 384
const cleanupSpy = vi.fn();
browserPool.on('instanceCleanedForResourceLimit', cleanupSpy);
// Trigger resource limit enforcement
await (browserPool as any).enforceResourceLimits();
expect(cleanupSpy).toHaveBeenCalledWith({ instanceId: instance.id });
});
it('should emit warnings for active instances exceeding limits', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
// Keep instance active
instance.isActive = true;
instance.metrics.memoryUsage = 300; // Exceeds limit
const warningSpy = vi.fn();
browserPool.on('resourceLimitWarning', warningSpy);
// Trigger resource limit enforcement
await (browserPool as any).enforceResourceLimits();
expect(warningSpy).toHaveBeenCalledWith({
instanceId: instance.id,
type: 'memory',
usage: 300,
limit: 256,
isActive: true
});
});
it('should enforce CPU limits', async () => {
await browserPool.initialize();
const instance = await browserPool.acquireInstance();
await browserPool.releaseInstance(instance.id);
// Set CPU usage to exceed limit
instance.metrics.cpuUsage = 80; // Exceeds 70% limit
const cleanupSpy = vi.fn();
browserPool.on('instanceCleanedForResourceLimit', cleanupSpy);
// Trigger resource limit enforcement
await (browserPool as any).enforceResourceLimits();
expect(cleanupSpy).toHaveBeenCalledWith({ instanceId: instance.id });
});
it('should respect maximum concurrent instances limit', async () => {
const smallConfig = { ...config, maxInstances: 2 };
await browserPool.shutdown();
browserPool = new BrowserPool(smallConfig);
await browserPool.initialize();
// Acquire all available instances
const instance1 = await browserPool.acquireInstance();
const instance2 = await browserPool.acquireInstance();
// Try to acquire one more - should wait or fail
const acquirePromise = browserPool.acquireInstance();
// Should not resolve immediately since we're at max capacity
let resolved = false;
acquirePromise.then(() => { resolved = true; }).catch(() => { resolved = true; });
// Wait a short time
await new Promise(resolve => setTimeout(resolve, 100));
expect(resolved).toBe(false);
// Release an instance
await browserPool.releaseInstance(instance1.id);
// Now the acquire should succeed
const instance3 = await acquirePromise;
expect(instance3).toBeDefined();
});
});
describe('Performance Monitoring and Alerting', () => {
let testRunner: TestRunner;
let testConfig: TestConfiguration;
beforeEach(() => {
testConfig = {
concurrentUsers: 2,
testDuration: 10,
rampUpTime: 2,
streamingUrl: 'https://example.com/stream',
requestParameters: [],
resourceLimits: {
maxMemoryPerInstance: 256,
maxCpuPercentage: 70,
maxConcurrentInstances: 5
}
};
testRunner = new TestRunner(testConfig);
});
it('should generate memory utilization alerts', () => {
const mockResourceStats = {
memoryUtilization: 85, // Above 80% threshold
cpuUtilization: 60,
instancesNearMemoryLimit: 2,
instancesNearCpuLimit: 0,
totalInstances: 3,
activeInstances: 2
};
const alerts = (testRunner as any).generateResourceAlerts(mockResourceStats);
const memoryAlert = alerts.find((alert: any) => alert.type === 'memory');
expect(memoryAlert).toBeDefined();
expect(memoryAlert.severity).toBe('warning');
expect(memoryAlert.message).toContain('High memory utilization: 85.0%');
});
it('should generate critical memory utilization alerts', () => {
const mockResourceStats = {
memoryUtilization: 95, // Above 90% threshold
cpuUtilization: 60,
instancesNearMemoryLimit: 3,
instancesNearCpuLimit: 0,
totalInstances: 3,
activeInstances: 3
};
const alerts = (testRunner as any).generateResourceAlerts(mockResourceStats);
const memoryAlert = alerts.find((alert: any) => alert.type === 'memory');
expect(memoryAlert).toBeDefined();
expect(memoryAlert.severity).toBe('critical');
expect(memoryAlert.message).toContain('Critical memory utilization: 95.0%');
});
it('should generate CPU utilization alerts', () => {
const mockResourceStats = {
memoryUtilization: 60,
cpuUtilization: 85, // Above 80% threshold
instancesNearMemoryLimit: 0,
instancesNearCpuLimit: 2,
totalInstances: 3,
activeInstances: 2
};
const alerts = (testRunner as any).generateResourceAlerts(mockResourceStats);
const cpuAlert = alerts.find((alert: any) => alert.type === 'cpu');
expect(cpuAlert).toBeDefined();
expect(cpuAlert.severity).toBe('warning');
expect(cpuAlert.message).toContain('High CPU utilization: 85.0%');
});
it('should generate instance limit alerts', () => {
const mockResourceStats = {
memoryUtilization: 60,
cpuUtilization: 60,
instancesNearMemoryLimit: 0,
instancesNearCpuLimit: 0,
totalInstances: 5, // At max limit
activeInstances: 5
};
const alerts = (testRunner as any).generateResourceAlerts(mockResourceStats);
const instanceAlert = alerts.find((alert: any) => alert.type === 'instance_limit');
expect(instanceAlert).toBeDefined();
expect(instanceAlert.severity).toBe('critical');
expect(instanceAlert.message).toContain('Near maximum instance limit: 5/5');
});
it('should generate performance alerts based on response times', () => {
// Mock network metrics with slow response times
const mockNetworkMetrics = [
{
url: 'https://example.com/api',
method: 'GET',
responseTime: 3000, // 3 seconds - above warning threshold
statusCode: 200,
timestamp: new Date(),
requestSize: 500,
responseSize: 1000
},
{
url: 'https://example.com/api2',
method: 'GET',
responseTime: 2500,
statusCode: 200,
timestamp: new Date(),
requestSize: 500,
responseSize: 1000
}
];
// Mock getAllNetworkMetrics to return our test data
(testRunner as any).getAllNetworkMetrics = vi.fn().mockReturnValue(mockNetworkMetrics);
const mockResourceStats = {
memoryUtilization: 60,
cpuUtilization: 60,
instancesNearMemoryLimit: 0,
instancesNearCpuLimit: 0,
totalInstances: 2,
activeInstances: 2
};
const alerts = (testRunner as any).generateResourceAlerts(mockResourceStats);
const performanceAlert = alerts.find((alert: any) => alert.type === 'performance');
expect(performanceAlert).toBeDefined();
expect(performanceAlert.severity).toBe('warning');
expect(performanceAlert.message).toContain('Slow response times: 2750ms average');
});
it('should provide detailed resource usage statistics', () => {
const resourceStats = testRunner.getResourceUsageStats();
expect(resourceStats).toHaveProperty('totalInstances');
expect(resourceStats).toHaveProperty('activeInstances');
expect(resourceStats).toHaveProperty('totalMemoryUsage');
expect(resourceStats).toHaveProperty('averageMemoryUsage');
expect(resourceStats).toHaveProperty('totalCpuUsage');
expect(resourceStats).toHaveProperty('averageCpuUsage');
expect(resourceStats).toHaveProperty('memoryUtilization');
expect(resourceStats).toHaveProperty('cpuUtilization');
expect(resourceStats).toHaveProperty('instancesNearMemoryLimit');
expect(resourceStats).toHaveProperty('instancesNearCpuLimit');
expect(resourceStats).toHaveProperty('resourceLimits');
});
it('should include resource utilization data in monitoring output', () => {
const monitoringData = testRunner.getMonitoringData();
expect(monitoringData).toHaveProperty('resourceUtilization');
expect(monitoringData.resourceUtilization).toHaveProperty('memoryUtilization');
expect(monitoringData.resourceUtilization).toHaveProperty('cpuUtilization');
expect(monitoringData.resourceUtilization).toHaveProperty('instancesNearMemoryLimit');
expect(monitoringData.resourceUtilization).toHaveProperty('instancesNearCpuLimit');
expect(monitoringData.resourceUtilization).toHaveProperty('resourceAlerts');
expect(Array.isArray(monitoringData.resourceUtilization.resourceAlerts)).toBe(true);
});
});
describe('Resource Consumption Benchmarks', () => {
let browserPool: BrowserPool;
let config: BrowserPoolConfig;
beforeEach(() => {
config = {
maxInstances: 10,
minInstances: 2,
resourceLimits: {
maxMemoryPerInstance: 512,
maxCpuPercentage: 80,
maxConcurrentInstances: 10
}
};
browserPool = new BrowserPool(config);
});
afterEach(async () => {
if (browserPool) {
await browserPool.shutdown();
}
});
it('should maintain memory usage within configured limits per instance', async () => {
await browserPool.initialize();
// Create multiple instances and check memory usage
const instances = [];
for (let i = 0; i < 5; i++) {
instances.push(await browserPool.acquireInstance());
}
const metrics = browserPool.getMetrics();
// Each instance should be within memory limits
for (const metric of metrics) {
expect(metric.memoryUsage).toBeLessThanOrEqual(config.resourceLimits.maxMemoryPerInstance);
}
// Clean up
for (const instance of instances) {
await browserPool.releaseInstance(instance.id);
}
});
it('should efficiently reuse browser instances to minimize resource overhead', async () => {
await browserPool.initialize();
// Acquire and release instances multiple times
const acquisitionTimes = [];
for (let i = 0; i < 10; i++) {
const startTime = Date.now();
const instance = await browserPool.acquireInstance();
const acquisitionTime = Date.now() - startTime;
acquisitionTimes.push(acquisitionTime);
await browserPool.releaseInstance(instance.id);
}
// After the first few acquisitions, subsequent ones should be faster due to reuse
const initialAcquisitions = acquisitionTimes.slice(0, 3);
const laterAcquisitions = acquisitionTimes.slice(3);
const avgInitial = initialAcquisitions.reduce((a, b) => a + b, 0) / initialAcquisitions.length;
const avgLater = laterAcquisitions.reduce((a, b) => a + b, 0) / laterAcquisitions.length;
// Later acquisitions should generally be faster (allowing some variance)
expect(avgLater).toBeLessThanOrEqual(avgInitial * 1.5);
});
it('should cleanup idle instances to free resources', async () => {
await browserPool.initialize();
// Create several instances beyond minimum
const instances = [];
for (let i = 0; i < 5; i++) {
instances.push(await browserPool.acquireInstance());
}
// Release them all
for (const instance of instances) {
await browserPool.releaseInstance(instance.id);
}
const statusAfterRelease = browserPool.getPoolStatus();
// Manually set lastUsed time to make instances appear idle
for (const [instanceId, instance] of (browserPool as any).instances) {
if (!instance.isActive) {
instance.lastUsed = new Date(Date.now() - 400000); // 400 seconds ago
}
}
// Force cleanup of idle instances
const cleanedCount = await browserPool.cleanupIdleInstances(300000); // 5 minutes
const finalStatus = browserPool.getPoolStatus();
// Should have cleaned up some instances if we had more than minimum
if (statusAfterRelease.totalInstances > config.minInstances) {
expect(cleanedCount).toBeGreaterThan(0);
expect(finalStatus.totalInstances).toBeLessThan(statusAfterRelease.totalInstances);
} else {
// If we only had minimum instances, nothing should be cleaned up
expect(cleanedCount).toBe(0);
}
// Should never go below minimum
expect(finalStatus.totalInstances).toBeGreaterThanOrEqual(config.minInstances);
});
it('should provide accurate resource utilization calculations', async () => {
await browserPool.initialize();
// Create instances with known resource usage
const instances = [];
for (let i = 0; i < 3; i++) {
const instance = await browserPool.acquireInstance();
instance.metrics.memoryUsage = 100 + (i * 50); // 100, 150, 200 MB
instance.metrics.cpuUsage = 20 + (i * 10); // 20, 30, 40%
instances.push(instance);
}
const resourceStats = browserPool.getResourceUsageStats();
// Verify calculations
expect(resourceStats.totalMemoryUsage).toBe(450); // 100 + 150 + 200
expect(resourceStats.averageMemoryUsage).toBe(150); // 450 / 3
expect(resourceStats.totalCpuUsage).toBe(90); // 20 + 30 + 40
expect(resourceStats.averageCpuUsage).toBe(30); // 90 / 3
// Memory utilization: (450 / (3 * 512)) * 100 = ~29.3%
expect(resourceStats.memoryUtilization).toBeCloseTo(29.3, 1);
// CPU utilization: (30 / 80) * 100 = 37.5%
expect(resourceStats.cpuUtilization).toBeCloseTo(37.5, 1);
// Clean up
for (const instance of instances) {
await browserPool.releaseInstance(instance.id);
}
});
it('should handle resource exhaustion gracefully', async () => {
// Create a pool with very limited resources for testing
const limitedConfig = {
...config,
maxInstances: 2,
resourceLimits: {
...config.resourceLimits,
maxMemoryPerInstance: 100 // Very low limit
}
};
await browserPool.shutdown();
browserPool = new BrowserPool(limitedConfig);
await browserPool.initialize();
const instances = [];
// Acquire instances and push them to memory limits
for (let i = 0; i < 2; i++) {
const instance = await browserPool.acquireInstance();
instance.metrics.memoryUsage = 95; // Near limit
instances.push(instance);
}
// Release instances
for (const instance of instances) {
await browserPool.releaseInstance(instance.id);
}
// Set one instance to exceed limits
instances[0].metrics.memoryUsage = 120; // Exceeds limit
const cleanupSpy = vi.fn();
const destroySpy = vi.fn();
browserPool.on('instanceCleanedForResourceLimit', cleanupSpy);
browserPool.on('instanceDestroyedForResourceLimit', destroySpy);
// Trigger resource enforcement
await (browserPool as any).enforceResourceLimits();
// Should have attempted cleanup or destruction
expect(cleanupSpy.mock.calls.length + destroySpy.mock.calls.length).toBeGreaterThan(0);
});
});
describe('Integration Performance Tests', () => {
it('should maintain performance under concurrent load', async () => {
const testConfig: TestConfiguration = {
concurrentUsers: 2, // Reduced for test stability
testDuration: 2, // Shorter duration for test
rampUpTime: 1,
streamingUrl: 'https://example.com/stream',
requestParameters: [],
resourceLimits: {
maxMemoryPerInstance: 256,
maxCpuPercentage: 70,
maxConcurrentInstances: 10
}
};
const testRunner = new TestRunner(testConfig);
// Test resource usage statistics without actually starting the test
const resourceStats = testRunner.getResourceUsageStats();
// Verify resource statistics structure
expect(resourceStats).toHaveProperty('totalInstances');
expect(resourceStats).toHaveProperty('activeInstances');
expect(resourceStats).toHaveProperty('memoryUtilization');
expect(resourceStats).toHaveProperty('cpuUtilization');
// Test monitoring data structure
const monitoringData = testRunner.getMonitoringData();
// Verify resource utilization is within acceptable bounds (should be 0 for new test runner)
expect(monitoringData.resourceUtilization.memoryUtilization).toBeLessThan(90);
expect(monitoringData.resourceUtilization.cpuUtilization).toBeLessThan(90);
// Verify monitoring data structure
expect(monitoringData).toHaveProperty('resourceUtilization');
expect(monitoringData.resourceUtilization).toHaveProperty('resourceAlerts');
expect(Array.isArray(monitoringData.resourceUtilization.resourceAlerts)).toBe(true);
// Test idle cleanup functionality
const cleanedCount = await testRunner.cleanupIdleInstances(0);
expect(cleanedCount).toBeGreaterThanOrEqual(0);
});
});
});