lightweight-browser-load-tester
Version:
A lightweight load testing tool using real browsers for streaming applications with DRM support
486 lines (483 loc) • 18.3 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResultsAggregator = void 0;
const prometheus_exporter_1 = require("../exporters/prometheus-exporter");
/**
* Aggregates test results from multiple browser instances and generates reports
*/
class ResultsAggregator {
constructor(prometheusConfig) {
this.testResults = {
summary: {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
peakConcurrentUsers: 0,
testDuration: 0
},
browserMetrics: [],
drmMetrics: [],
networkMetrics: [],
errors: []
};
this.startTime = new Date();
// Initialize Prometheus exporter if configuration is provided
if (prometheusConfig) {
this.prometheusExporter = new prometheus_exporter_1.PrometheusExporter(prometheusConfig);
}
}
/**
* Add browser metrics from a single instance
*/
addBrowserMetrics(metrics) {
this.testResults.browserMetrics.push(metrics);
// Export to Prometheus in real-time if configured
if (this.prometheusExporter) {
this.prometheusExporter.exportBrowserMetrics(metrics).catch(error => {
console.error('Failed to export browser metrics to Prometheus:', error);
});
}
}
/**
* Add DRM metrics
*/
addDRMMetrics(metrics) {
this.testResults.drmMetrics.push(metrics);
// Export to Prometheus in real-time if configured
if (this.prometheusExporter) {
this.prometheusExporter.exportDRMMetrics(metrics).catch(error => {
console.error('Failed to export DRM metrics to Prometheus:', error);
});
}
}
/**
* Add network metrics
*/
addNetworkMetrics(metrics) {
this.testResults.networkMetrics.push(...metrics);
// Export to Prometheus in real-time if configured
if (this.prometheusExporter && metrics.length > 0) {
this.prometheusExporter.exportNetworkMetrics(metrics).catch(error => {
console.error('Failed to export network metrics to Prometheus:', error);
});
}
}
/**
* Add error log entry
*/
addError(error) {
this.testResults.errors.push(error);
// Export to Prometheus in real-time if configured
if (this.prometheusExporter) {
this.prometheusExporter.exportErrorMetrics([error]).catch(error => {
console.error('Failed to export error metrics to Prometheus:', error);
});
}
}
/**
* Finalize test results and calculate summary statistics
*/
finalize() {
this.endTime = new Date();
this.calculateSummary();
// Export final summary to Prometheus if configured
if (this.prometheusExporter) {
this.prometheusExporter.exportTestSummary(this.testResults.summary).catch(error => {
console.error('Failed to export test summary to Prometheus:', error);
});
}
}
/**
* Get the aggregated test results
*/
getResults() {
return { ...this.testResults };
}
/**
* Shutdown the aggregator and flush any remaining Prometheus metrics
*/
async shutdown() {
if (this.prometheusExporter) {
await this.prometheusExporter.shutdown();
}
}
/**
* Generate reports in specified formats
*/
async generateReports(config) {
if (!this.endTime) {
this.finalize();
}
for (const format of config.formats) {
switch (format) {
case 'json':
await this.generateJSONReport(config);
break;
case 'html':
await this.generateHTMLReport(config);
break;
case 'csv':
await this.generateCSVReport(config);
break;
}
}
}
/**
* Calculate summary statistics from collected metrics
*/
calculateSummary() {
const networkMetrics = this.testResults.networkMetrics;
const browserMetrics = this.testResults.browserMetrics;
// Calculate request statistics
this.testResults.summary.totalRequests = networkMetrics.length;
this.testResults.summary.successfulRequests = networkMetrics.filter(m => m.statusCode >= 200 && m.statusCode < 400).length;
this.testResults.summary.failedRequests =
this.testResults.summary.totalRequests - this.testResults.summary.successfulRequests;
// Calculate average response time
if (networkMetrics.length > 0) {
this.testResults.summary.averageResponseTime =
networkMetrics.reduce((sum, m) => sum + m.responseTime, 0) / networkMetrics.length;
}
// Calculate peak concurrent users (max active browser instances)
this.testResults.summary.peakConcurrentUsers = browserMetrics.length;
// Calculate test duration
if (this.endTime) {
this.testResults.summary.testDuration =
(this.endTime.getTime() - this.startTime.getTime()) / 1000;
}
}
/**
* Generate JSON report
*/
async generateJSONReport(config) {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
const path = await Promise.resolve().then(() => __importStar(require('path')));
const outputPath = config.outputPath || './test-results';
const filename = path.join(outputPath, `load-test-results-${Date.now()}.json`);
await fs.mkdir(outputPath, { recursive: true });
await fs.writeFile(filename, JSON.stringify(this.testResults, null, 2));
}
/**
* Generate HTML report
*/
async generateHTMLReport(config) {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
const path = await Promise.resolve().then(() => __importStar(require('path')));
const outputPath = config.outputPath || './test-results';
const filename = path.join(outputPath, `load-test-results-${Date.now()}.html`);
const html = this.generateHTMLContent(config);
await fs.mkdir(outputPath, { recursive: true });
await fs.writeFile(filename, html);
}
/**
* Generate CSV report
*/
async generateCSVReport(config) {
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
const path = await Promise.resolve().then(() => __importStar(require('path')));
const outputPath = config.outputPath || './test-results';
const timestamp = Date.now();
await fs.mkdir(outputPath, { recursive: true });
// Generate summary CSV
const summaryCSV = this.generateSummaryCSV();
await fs.writeFile(path.join(outputPath, `load-test-summary-${timestamp}.csv`), summaryCSV);
// Generate network metrics CSV
const networkCSV = this.generateNetworkMetricsCSV();
await fs.writeFile(path.join(outputPath, `load-test-network-${timestamp}.csv`), networkCSV);
// Generate browser metrics CSV
const browserCSV = this.generateBrowserMetricsCSV();
await fs.writeFile(path.join(outputPath, `load-test-browser-${timestamp}.csv`), browserCSV);
} /**
* Generate HTML content for the report
*/
generateHTMLContent(config) {
const summary = this.testResults.summary;
const errors = this.testResults.errors;
const drmMetrics = this.testResults.drmMetrics;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Load Test Results</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.header { text-align: center; margin-bottom: 30px; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
.metric-card { background: #f8f9fa; padding: 15px; border-radius: 6px; text-align: center; }
.metric-value { font-size: 2em; font-weight: bold; color: #007bff; }
.metric-label { color: #666; margin-top: 5px; }
.section { margin-bottom: 30px; }
.section h2 { border-bottom: 2px solid #007bff; padding-bottom: 10px; }
table { width: 100%; border-collapse: collapse; margin-top: 15px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f8f9fa; font-weight: bold; }
.error { color: #dc3545; }
.success { color: #28a745; }
.warning { color: #ffc107; }
.chart-placeholder { background: #f8f9fa; height: 200px; display: flex; align-items: center; justify-content: center; border-radius: 6px; margin: 15px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Load Test Results</h1>
<p>Generated on ${new Date().toLocaleString()}</p>
<p>Test Duration: ${summary.testDuration.toFixed(2)} seconds</p>
</div>
<div class="section">
<h2>Summary</h2>
<div class="summary">
<div class="metric-card">
<div class="metric-value">${summary.totalRequests}</div>
<div class="metric-label">Total Requests</div>
</div>
<div class="metric-card">
<div class="metric-value ${summary.successfulRequests > 0 ? 'success' : ''}">${summary.successfulRequests}</div>
<div class="metric-label">Successful Requests</div>
</div>
<div class="metric-card">
<div class="metric-value ${summary.failedRequests > 0 ? 'error' : ''}">${summary.failedRequests}</div>
<div class="metric-label">Failed Requests</div>
</div>
<div class="metric-card">
<div class="metric-value">${summary.averageResponseTime.toFixed(2)}ms</div>
<div class="metric-label">Avg Response Time</div>
</div>
<div class="metric-card">
<div class="metric-value">${summary.peakConcurrentUsers}</div>
<div class="metric-label">Peak Concurrent Users</div>
</div>
<div class="metric-card">
<div class="metric-value">${((summary.successfulRequests / summary.totalRequests) * 100).toFixed(1)}%</div>
<div class="metric-label">Success Rate</div>
</div>
</div>
</div>
${config.includeDetailedMetrics ? this.generateBrowserMetricsHTML() : ''}
${drmMetrics.length > 0 ? this.generateDRMMetricsHTML() : ''}
${config.includeErrorAnalysis && errors.length > 0 ? this.generateErrorAnalysisHTML() : ''}
</div>
</body>
</html>`;
}
/**
* Generate browser metrics section for HTML report
*/
generateBrowserMetricsHTML() {
const browserMetrics = this.testResults.browserMetrics;
if (browserMetrics.length === 0) {
return '';
}
const rows = browserMetrics.map(metrics => `
<tr>
<td>${metrics.instanceId}</td>
<td>${metrics.memoryUsage.toFixed(2)} MB</td>
<td>${metrics.cpuUsage.toFixed(1)}%</td>
<td>${metrics.requestCount}</td>
<td>${metrics.errorCount}</td>
<td>${metrics.uptime.toFixed(2)}s</td>
</tr>
`).join('');
return `
<div class="section">
<h2>Browser Instance Metrics</h2>
<table>
<thead>
<tr>
<th>Instance ID</th>
<th>Memory Usage</th>
<th>CPU Usage</th>
<th>Requests</th>
<th>Errors</th>
<th>Uptime</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`;
}
/**
* Generate DRM metrics section for HTML report
*/
generateDRMMetricsHTML() {
const drmMetrics = this.testResults.drmMetrics;
const rows = drmMetrics.map(metrics => `
<tr>
<td>${metrics.drmType}</td>
<td>${metrics.licenseRequestCount}</td>
<td>${metrics.averageLicenseTime.toFixed(2)}ms</td>
<td>${(metrics.licenseSuccessRate * 100).toFixed(1)}%</td>
<td>${metrics.errors.length}</td>
</tr>
`).join('');
return `
<div class="section">
<h2>DRM Metrics</h2>
<table>
<thead>
<tr>
<th>DRM Type</th>
<th>License Requests</th>
<th>Avg License Time</th>
<th>Success Rate</th>
<th>Errors</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`;
}
/**
* Generate error analysis section for HTML report
*/
generateErrorAnalysisHTML() {
const errors = this.testResults.errors;
const errorRows = errors.slice(0, 50).map(error => `
<tr>
<td>${error.timestamp.toLocaleString()}</td>
<td><span class="${error.level}">${error.level.toUpperCase()}</span></td>
<td>${error.message}</td>
<td>${error.context ? JSON.stringify(error.context) : '-'}</td>
</tr>
`).join('');
return `
<div class="section">
<h2>Error Analysis</h2>
<p>Showing ${Math.min(50, errors.length)} of ${errors.length} total errors</p>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Level</th>
<th>Message</th>
<th>Context</th>
</tr>
</thead>
<tbody>
${errorRows}
</tbody>
</table>
</div>
`;
}
/**
* Generate summary CSV content
*/
generateSummaryCSV() {
const summary = this.testResults.summary;
const headers = [
'Total Requests',
'Successful Requests',
'Failed Requests',
'Average Response Time (ms)',
'Peak Concurrent Users',
'Test Duration (s)',
'Success Rate (%)'
];
const values = [
summary.totalRequests,
summary.successfulRequests,
summary.failedRequests,
summary.averageResponseTime.toFixed(2),
summary.peakConcurrentUsers,
summary.testDuration.toFixed(2),
((summary.successfulRequests / summary.totalRequests) * 100).toFixed(1)
];
return headers.join(',') + '\n' + values.join(',');
}
/**
* Generate network metrics CSV content
*/
generateNetworkMetricsCSV() {
const headers = [
'URL',
'Method',
'Response Time (ms)',
'Status Code',
'Timestamp',
'Request Size (bytes)',
'Response Size (bytes)',
'Streaming Related',
'Streaming Type'
];
const rows = this.testResults.networkMetrics.map(metric => [
`"${metric.url}"`,
metric.method,
metric.responseTime,
metric.statusCode,
metric.timestamp.toISOString(),
metric.requestSize,
metric.responseSize,
metric.isStreamingRelated || false,
metric.streamingType || ''
].join(','));
return headers.join(',') + '\n' + rows.join('\n');
}
/**
* Generate browser metrics CSV content
*/
generateBrowserMetricsCSV() {
const headers = [
'Instance ID',
'Memory Usage (MB)',
'CPU Usage (%)',
'Request Count',
'Error Count',
'Uptime (s)'
];
const rows = this.testResults.browserMetrics.map(metric => [
metric.instanceId,
metric.memoryUsage.toFixed(2),
metric.cpuUsage.toFixed(1),
metric.requestCount,
metric.errorCount,
metric.uptime.toFixed(2)
].join(','));
return headers.join(',') + '\n' + rows.join('\n');
}
}
exports.ResultsAggregator = ResultsAggregator;
//# sourceMappingURL=results-aggregator.js.map
;