Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
32
marketing-agent/performance-tests/package.json
Normal file
32
marketing-agent/performance-tests/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "performance-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "Performance benchmarking for Telegram Marketing Intelligence Agent System",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "node src/runner.js",
|
||||
"test:api": "node src/tests/api-benchmark.js",
|
||||
"test:load": "node src/tests/load-test.js",
|
||||
"test:stress": "node src/tests/stress-test.js",
|
||||
"test:database": "node src/tests/database-benchmark.js",
|
||||
"test:message": "node src/tests/message-throughput.js",
|
||||
"test:all": "npm run test:api && npm run test:load && npm run test:database && npm run test:message",
|
||||
"report": "node src/report-generator.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"autocannon": "^7.14.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-table3": "^0.6.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"k6": "^0.47.0",
|
||||
"mongodb": "^6.3.0",
|
||||
"ora": "^7.0.1",
|
||||
"socket.io-client": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0"
|
||||
}
|
||||
}
|
||||
69
marketing-agent/performance-tests/run-tests.sh
Executable file
69
marketing-agent/performance-tests/run-tests.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run Performance Tests for Telegram Marketing Intelligence Agent System
|
||||
|
||||
echo "🚀 Telegram Marketing Intelligence Agent - Performance Test Suite"
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
|
||||
# Change to the performance-tests directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Check if dependencies are installed
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Choose which tests to run:"
|
||||
echo "1) All tests"
|
||||
echo "2) API Benchmark only"
|
||||
echo "3) Load Test only"
|
||||
echo "4) Stress Test only"
|
||||
echo "5) Database Benchmark only"
|
||||
echo "6) Message Throughput only"
|
||||
echo ""
|
||||
|
||||
read -p "Enter your choice (1-6): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
echo ""
|
||||
echo "Running all performance tests..."
|
||||
node src/runner.js
|
||||
;;
|
||||
2)
|
||||
echo ""
|
||||
echo "Running API Benchmark..."
|
||||
node src/tests/api-benchmark.js
|
||||
;;
|
||||
3)
|
||||
echo ""
|
||||
echo "Running Load Test..."
|
||||
node src/tests/load-test.js
|
||||
;;
|
||||
4)
|
||||
echo ""
|
||||
echo "Running Stress Test..."
|
||||
node src/tests/stress-test.js
|
||||
;;
|
||||
5)
|
||||
echo ""
|
||||
echo "Running Database Benchmark..."
|
||||
node src/tests/database-benchmark.js
|
||||
;;
|
||||
6)
|
||||
echo ""
|
||||
echo "Running Message Throughput Test..."
|
||||
node src/tests/message-throughput.js
|
||||
;;
|
||||
*)
|
||||
echo "Invalid choice. Exiting."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "✅ Performance tests completed!"
|
||||
echo "📊 Results saved in ./reports/ directory"
|
||||
118
marketing-agent/performance-tests/src/config.js
Normal file
118
marketing-agent/performance-tests/src/config.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
// API Gateway
|
||||
apiGateway: {
|
||||
url: process.env.API_GATEWAY_URL || 'http://localhost:3000',
|
||||
apiKey: process.env.API_KEY || 'test-api-key'
|
||||
},
|
||||
|
||||
// Service URLs
|
||||
services: {
|
||||
orchestrator: process.env.ORCHESTRATOR_URL || 'http://localhost:3001',
|
||||
claudeAgent: process.env.CLAUDE_AGENT_URL || 'http://localhost:3002',
|
||||
gramjsAdapter: process.env.GRAMJS_ADAPTER_URL || 'http://localhost:3003',
|
||||
analytics: process.env.ANALYTICS_URL || 'http://localhost:3005',
|
||||
billing: process.env.BILLING_URL || 'http://localhost:3010'
|
||||
},
|
||||
|
||||
// Database
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/marketing_agent'
|
||||
},
|
||||
|
||||
// Test Configuration
|
||||
test: {
|
||||
// API Benchmark
|
||||
api: {
|
||||
duration: 30, // seconds
|
||||
connections: 10,
|
||||
pipelining: 1,
|
||||
warmup: 5 // seconds
|
||||
},
|
||||
|
||||
// Load Test
|
||||
load: {
|
||||
stages: [
|
||||
{ duration: '30s', target: 10 }, // Ramp up to 10 users
|
||||
{ duration: '1m', target: 50 }, // Stay at 50 users
|
||||
{ duration: '30s', target: 100 }, // Ramp up to 100 users
|
||||
{ duration: '2m', target: 100 }, // Stay at 100 users
|
||||
{ duration: '30s', target: 0 } // Ramp down to 0 users
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(95)<500'], // 95% of requests must complete below 500ms
|
||||
http_req_failed: ['rate<0.1'], // Error rate must be below 10%
|
||||
}
|
||||
},
|
||||
|
||||
// Stress Test
|
||||
stress: {
|
||||
stages: [
|
||||
{ duration: '30s', target: 100 }, // Ramp up to 100 users
|
||||
{ duration: '30s', target: 200 }, // Ramp up to 200 users
|
||||
{ duration: '30s', target: 500 }, // Ramp up to 500 users
|
||||
{ duration: '1m', target: 1000 }, // Ramp up to 1000 users
|
||||
{ duration: '30s', target: 0 } // Ramp down to 0 users
|
||||
]
|
||||
},
|
||||
|
||||
// Database Benchmark
|
||||
database: {
|
||||
operations: {
|
||||
insert: 10000,
|
||||
find: 50000,
|
||||
update: 10000,
|
||||
aggregate: 1000
|
||||
},
|
||||
batchSize: 100
|
||||
},
|
||||
|
||||
// Message Throughput
|
||||
message: {
|
||||
totalMessages: 10000,
|
||||
concurrentSenders: 10,
|
||||
messageSize: 1000, // bytes
|
||||
batchSize: 100
|
||||
}
|
||||
},
|
||||
|
||||
// Performance Thresholds
|
||||
thresholds: {
|
||||
api: {
|
||||
responseTime: {
|
||||
p50: 50, // 50th percentile
|
||||
p75: 100, // 75th percentile
|
||||
p90: 200, // 90th percentile
|
||||
p95: 300, // 95th percentile
|
||||
p99: 500 // 99th percentile
|
||||
},
|
||||
throughput: {
|
||||
min: 1000 // requests per second
|
||||
},
|
||||
errorRate: {
|
||||
max: 0.01 // 1%
|
||||
}
|
||||
},
|
||||
database: {
|
||||
operations: {
|
||||
insert: 5000, // ops/sec
|
||||
find: 10000, // ops/sec
|
||||
update: 3000, // ops/sec
|
||||
aggregate: 500 // ops/sec
|
||||
}
|
||||
},
|
||||
message: {
|
||||
throughput: 1000, // messages/sec
|
||||
latency: 100 // ms
|
||||
}
|
||||
},
|
||||
|
||||
// Report Configuration
|
||||
report: {
|
||||
outputDir: './reports',
|
||||
formats: ['json', 'html', 'csv']
|
||||
}
|
||||
};
|
||||
238
marketing-agent/performance-tests/src/runner.js
Normal file
238
marketing-agent/performance-tests/src/runner.js
Normal file
@@ -0,0 +1,238 @@
|
||||
import chalk from 'chalk';
|
||||
import { spawn } from 'child_process';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { config } from './config.js';
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'API Benchmark',
|
||||
script: 'src/tests/api-benchmark.js',
|
||||
description: 'Tests API endpoint performance and latency'
|
||||
},
|
||||
{
|
||||
name: 'Load Test',
|
||||
script: 'src/tests/load-test.js',
|
||||
description: 'Simulates realistic user load patterns'
|
||||
},
|
||||
{
|
||||
name: 'Stress Test',
|
||||
script: 'src/tests/stress-test.js',
|
||||
description: 'Tests system limits and breaking points'
|
||||
},
|
||||
{
|
||||
name: 'Database Benchmark',
|
||||
script: 'src/tests/database-benchmark.js',
|
||||
description: 'Tests database operation performance'
|
||||
},
|
||||
{
|
||||
name: 'Message Throughput',
|
||||
script: 'src/tests/message-throughput.js',
|
||||
description: 'Tests message sending capacity'
|
||||
}
|
||||
];
|
||||
|
||||
async function runTest(test) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const spinner = ora(`Running ${test.name}...`).start();
|
||||
|
||||
const child = spawn('node', [test.script], {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env }
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
spinner.succeed(`${test.name} completed`);
|
||||
resolve({ test, output, success: true });
|
||||
} else {
|
||||
spinner.fail(`${test.name} failed`);
|
||||
resolve({ test, output, errorOutput, success: false });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
spinner.fail(`${test.name} error`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function generateSummaryReport(results) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const reportPath = path.join(config.report.outputDir, `performance-summary-${timestamp.replace(/:/g, '-')}.md`);
|
||||
|
||||
let report = `# Performance Test Summary\n\n`;
|
||||
report += `Generated: ${timestamp}\n\n`;
|
||||
report += `## Test Results\n\n`;
|
||||
|
||||
const passed = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
report += `- **Total Tests**: ${results.length}\n`;
|
||||
report += `- **Passed**: ${passed}\n`;
|
||||
report += `- **Failed**: ${failed}\n\n`;
|
||||
|
||||
report += `## Individual Test Results\n\n`;
|
||||
|
||||
for (const result of results) {
|
||||
report += `### ${result.test.name}\n\n`;
|
||||
report += `**Status**: ${result.success ? '✅ PASSED' : '❌ FAILED'}\n\n`;
|
||||
report += `**Description**: ${result.test.description}\n\n`;
|
||||
|
||||
if (!result.success && result.errorOutput) {
|
||||
report += `**Error Output**:\n\`\`\`\n${result.errorOutput}\n\`\`\`\n\n`;
|
||||
}
|
||||
|
||||
// Extract key metrics from output
|
||||
const metrics = extractMetrics(result.output);
|
||||
if (metrics.length > 0) {
|
||||
report += `**Key Metrics**:\n`;
|
||||
metrics.forEach(metric => {
|
||||
report += `- ${metric}\n`;
|
||||
});
|
||||
report += `\n`;
|
||||
}
|
||||
}
|
||||
|
||||
report += `## Recommendations\n\n`;
|
||||
report += generateRecommendations(results);
|
||||
|
||||
await fs.mkdir(config.report.outputDir, { recursive: true });
|
||||
await fs.writeFile(reportPath, report);
|
||||
|
||||
return reportPath;
|
||||
}
|
||||
|
||||
function extractMetrics(output) {
|
||||
const metrics = [];
|
||||
const lines = output.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('req/sec') ||
|
||||
line.includes('msg/sec') ||
|
||||
line.includes('ops/sec') ||
|
||||
line.includes('latency') ||
|
||||
line.includes('throughput')) {
|
||||
metrics.push(line.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
function generateRecommendations(results) {
|
||||
const recommendations = [];
|
||||
|
||||
// Analyze results and generate recommendations
|
||||
for (const result of results) {
|
||||
if (!result.success) {
|
||||
recommendations.push(`- **${result.test.name}**: Test failed. Investigate system capacity and error logs.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const output = result.output.toLowerCase();
|
||||
|
||||
if (output.includes('threshold') && output.includes('exceed')) {
|
||||
recommendations.push(`- **${result.test.name}**: Performance thresholds exceeded. Consider optimization or scaling.`);
|
||||
}
|
||||
|
||||
if (output.includes('error rate') && output.includes('high')) {
|
||||
recommendations.push(`- **${result.test.name}**: High error rate detected. Review error handling and system stability.`);
|
||||
}
|
||||
|
||||
if (output.includes('degradation')) {
|
||||
recommendations.push(`- **${result.test.name}**: Performance degradation detected. Analyze bottlenecks and optimize critical paths.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
recommendations.push('- All tests passed within acceptable thresholds. Continue monitoring for performance regressions.');
|
||||
}
|
||||
|
||||
return recommendations.join('\n');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue('🚀 Performance Test Suite'));
|
||||
console.log('=' .repeat(60));
|
||||
console.log(chalk.gray('This suite will run comprehensive performance tests'));
|
||||
console.log(chalk.gray('for the Telegram Marketing Intelligence Agent System\n'));
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let testsToRun = tests;
|
||||
|
||||
// Filter tests if specific ones are requested
|
||||
if (args.length > 0) {
|
||||
testsToRun = tests.filter(test =>
|
||||
args.some(arg => test.name.toLowerCase().includes(arg.toLowerCase()))
|
||||
);
|
||||
|
||||
if (testsToRun.length === 0) {
|
||||
console.error(chalk.red('No matching tests found'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`Running ${testsToRun.length} test(s):\n`));
|
||||
testsToRun.forEach(test => {
|
||||
console.log(` • ${test.name}: ${chalk.gray(test.description)}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of testsToRun) {
|
||||
try {
|
||||
const result = await runTest(test);
|
||||
results.push(result);
|
||||
|
||||
// Add delay between tests
|
||||
if (testsToRun.indexOf(test) < testsToRun.length - 1) {
|
||||
console.log(chalk.gray('\nWaiting before next test...\n'));
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error running ${test.name}:`), error);
|
||||
results.push({ test, success: false, errorOutput: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Generate summary report
|
||||
console.log('\n' + chalk.bold.blue('Generating Summary Report...'));
|
||||
const reportPath = await generateSummaryReport(results);
|
||||
console.log(chalk.green(`✓ Summary report saved to: ${reportPath}`));
|
||||
|
||||
// Display final summary
|
||||
console.log('\n' + chalk.bold('Final Summary'));
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const passed = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
console.log(`Total: ${results.length} | ` +
|
||||
chalk.green(`Passed: ${passed}`) + ' | ' +
|
||||
chalk.red(`Failed: ${failed}`)
|
||||
);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\n' + chalk.red('❌ Some tests failed. Please review the results.'));
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n' + chalk.green('✅ All tests passed successfully!'));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
173
marketing-agent/performance-tests/src/tests/api-benchmark.js
Normal file
173
marketing-agent/performance-tests/src/tests/api-benchmark.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import autocannon from 'autocannon';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { config } from '../config.js';
|
||||
import { saveResults } from '../utils/results.js';
|
||||
|
||||
const endpoints = [
|
||||
{
|
||||
name: 'Health Check',
|
||||
method: 'GET',
|
||||
url: '/health'
|
||||
},
|
||||
{
|
||||
name: 'Get Campaigns',
|
||||
method: 'GET',
|
||||
url: '/api/v1/orchestrator/campaigns',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-token',
|
||||
'X-API-Key': config.apiGateway.apiKey
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Create Campaign',
|
||||
method: 'POST',
|
||||
url: '/api/v1/orchestrator/campaigns',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-token',
|
||||
'X-API-Key': config.apiGateway.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'Performance Test Campaign',
|
||||
description: 'Auto-generated campaign for performance testing',
|
||||
targetAudience: {
|
||||
groups: ['test-group-1'],
|
||||
tags: ['performance-test']
|
||||
},
|
||||
message: {
|
||||
content: 'This is a performance test message',
|
||||
type: 'text'
|
||||
},
|
||||
schedule: {
|
||||
type: 'immediate'
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'Get Analytics',
|
||||
method: 'GET',
|
||||
url: '/api/v1/analytics/campaigns/overview',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-token',
|
||||
'X-API-Key': config.apiGateway.apiKey
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Check Compliance',
|
||||
method: 'POST',
|
||||
url: '/api/v1/safety/check',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-token',
|
||||
'X-API-Key': config.apiGateway.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: 'Test message for compliance check',
|
||||
context: {
|
||||
campaignId: 'test-campaign',
|
||||
targetGroups: ['test-group']
|
||||
}
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
async function runBenchmark(endpoint) {
|
||||
const spinner = ora(`Testing ${endpoint.name}...`).start();
|
||||
|
||||
const instance = autocannon({
|
||||
url: config.apiGateway.url + endpoint.url,
|
||||
method: endpoint.method,
|
||||
headers: endpoint.headers,
|
||||
body: endpoint.body,
|
||||
connections: config.test.api.connections,
|
||||
pipelining: config.test.api.pipelining,
|
||||
duration: config.test.api.duration,
|
||||
warmup: config.test.api.warmup
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
instance.on('done', (results) => {
|
||||
spinner.succeed(`${endpoint.name} completed`);
|
||||
resolve({
|
||||
endpoint: endpoint.name,
|
||||
results
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function analyzeResults(results) {
|
||||
console.log('\n' + chalk.bold.blue('API Benchmark Results'));
|
||||
console.log('=' .repeat(80));
|
||||
|
||||
const summary = [];
|
||||
|
||||
results.forEach(({ endpoint, results }) => {
|
||||
const analysis = {
|
||||
endpoint,
|
||||
requests: results.requests.total,
|
||||
throughput: results.throughput.mean,
|
||||
latency: {
|
||||
p50: results.latency.p50,
|
||||
p90: results.latency.p90,
|
||||
p95: results.latency.p95,
|
||||
p99: results.latency.p99
|
||||
},
|
||||
errors: results.errors,
|
||||
timeouts: results.timeouts
|
||||
};
|
||||
|
||||
summary.push(analysis);
|
||||
|
||||
console.log(`\n${chalk.yellow(endpoint)}`);
|
||||
console.log(` Requests: ${chalk.green(results.requests.total)}`);
|
||||
console.log(` Throughput: ${chalk.green(results.throughput.mean.toFixed(2))} req/sec`);
|
||||
console.log(` Latency:`);
|
||||
console.log(` p50: ${chalk.cyan(results.latency.p50)} ms`);
|
||||
console.log(` p90: ${chalk.cyan(results.latency.p90)} ms`);
|
||||
console.log(` p95: ${chalk.cyan(results.latency.p95)} ms`);
|
||||
console.log(` p99: ${chalk.cyan(results.latency.p99)} ms`);
|
||||
|
||||
// Check against thresholds
|
||||
const thresholds = config.thresholds.api;
|
||||
|
||||
if (results.latency.p95 > thresholds.responseTime.p95) {
|
||||
console.log(chalk.red(` ⚠️ P95 latency exceeds threshold (${thresholds.responseTime.p95}ms)`));
|
||||
}
|
||||
|
||||
if (results.errors > 0) {
|
||||
console.log(chalk.red(` ⚠️ ${results.errors} errors occurred`));
|
||||
}
|
||||
|
||||
if (results.timeouts > 0) {
|
||||
console.log(chalk.red(` ⚠️ ${results.timeouts} timeouts occurred`));
|
||||
}
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.green('Starting API Benchmark Tests'));
|
||||
console.log(`Testing against: ${config.apiGateway.url}`);
|
||||
console.log(`Duration: ${config.test.api.duration}s per endpoint`);
|
||||
console.log(`Connections: ${config.test.api.connections}`);
|
||||
console.log(`Warmup: ${config.test.api.warmup}s\n`);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
const result = await runBenchmark(endpoint);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
const summary = await analyzeResults(results);
|
||||
|
||||
// Save results
|
||||
await saveResults('api-benchmark', summary);
|
||||
|
||||
console.log('\n' + chalk.green('✓ API Benchmark completed'));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,395 @@
|
||||
import { MongoClient } from 'mongodb';
|
||||
import Benchmark from 'benchmark';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { config } from '../config.js';
|
||||
import { saveResults } from '../utils/results.js';
|
||||
|
||||
let client;
|
||||
let db;
|
||||
let collections = {};
|
||||
|
||||
const testData = {
|
||||
campaigns: [],
|
||||
messages: [],
|
||||
users: [],
|
||||
analytics: []
|
||||
};
|
||||
|
||||
// Generate test data
|
||||
function generateTestData() {
|
||||
const spinner = ora('Generating test data...').start();
|
||||
|
||||
// Generate campaigns
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
testData.campaigns.push({
|
||||
name: `Campaign ${i}`,
|
||||
description: `Description for campaign ${i}`,
|
||||
tenantId: `tenant-${i % 10}`,
|
||||
targetAudience: {
|
||||
groups: [`group-${i % 20}`, `group-${(i + 1) % 20}`],
|
||||
tags: [`tag-${i % 50}`, `tag-${(i + 1) % 50}`],
|
||||
filters: {
|
||||
location: `city-${i % 100}`,
|
||||
age: { min: 18, max: 65 }
|
||||
}
|
||||
},
|
||||
message: {
|
||||
content: `Message content ${i}`.repeat(10),
|
||||
type: 'text',
|
||||
attachments: []
|
||||
},
|
||||
schedule: {
|
||||
type: i % 2 === 0 ? 'immediate' : 'scheduled',
|
||||
scheduledAt: new Date(Date.now() + i * 3600000)
|
||||
},
|
||||
status: ['draft', 'active', 'completed', 'paused'][i % 4],
|
||||
metrics: {
|
||||
sent: Math.floor(Math.random() * 10000),
|
||||
delivered: Math.floor(Math.random() * 9000),
|
||||
read: Math.floor(Math.random() * 5000),
|
||||
clicked: Math.floor(Math.random() * 1000)
|
||||
},
|
||||
createdAt: new Date(Date.now() - i * 86400000),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// Generate messages
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
testData.messages.push({
|
||||
campaignId: testData.campaigns[i % 1000]._id,
|
||||
tenantId: `tenant-${i % 10}`,
|
||||
recipient: {
|
||||
userId: `user-${i}`,
|
||||
chatId: `chat-${i}`,
|
||||
username: `user${i}`
|
||||
},
|
||||
content: `Message ${i}`,
|
||||
status: ['pending', 'sent', 'delivered', 'failed'][i % 4],
|
||||
attempts: Math.floor(Math.random() * 3),
|
||||
sentAt: new Date(),
|
||||
deliveredAt: i % 4 >= 2 ? new Date() : null,
|
||||
error: i % 4 === 3 ? 'Network error' : null
|
||||
});
|
||||
}
|
||||
|
||||
// Generate users
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
testData.users.push({
|
||||
userId: `user-${i}`,
|
||||
tenantId: `tenant-${i % 10}`,
|
||||
username: `user${i}`,
|
||||
firstName: `First${i}`,
|
||||
lastName: `Last${i}`,
|
||||
groups: [`group-${i % 20}`, `group-${(i + 1) % 20}`],
|
||||
tags: [`tag-${i % 50}`, `tag-${(i + 1) % 50}`],
|
||||
metadata: {
|
||||
location: `city-${i % 100}`,
|
||||
age: 18 + (i % 47),
|
||||
interests: ['tech', 'business', 'marketing', 'sales'][i % 4]
|
||||
},
|
||||
stats: {
|
||||
messagesReceived: Math.floor(Math.random() * 100),
|
||||
messagesRead: Math.floor(Math.random() * 80),
|
||||
lastActive: new Date()
|
||||
},
|
||||
createdAt: new Date(Date.now() - i * 86400000)
|
||||
});
|
||||
}
|
||||
|
||||
// Generate analytics events
|
||||
for (let i = 0; i < 50000; i++) {
|
||||
testData.analytics.push({
|
||||
eventType: ['message_sent', 'message_delivered', 'message_read', 'link_clicked'][i % 4],
|
||||
tenantId: `tenant-${i % 10}`,
|
||||
campaignId: `campaign-${i % 1000}`,
|
||||
userId: `user-${i % 5000}`,
|
||||
messageId: `message-${i % 10000}`,
|
||||
timestamp: new Date(Date.now() - Math.random() * 86400000 * 30),
|
||||
metadata: {
|
||||
deviceType: ['mobile', 'desktop', 'tablet'][i % 3],
|
||||
os: ['iOS', 'Android', 'Windows', 'MacOS'][i % 4],
|
||||
browser: ['Chrome', 'Safari', 'Firefox', 'Edge'][i % 4]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
spinner.succeed('Test data generated');
|
||||
}
|
||||
|
||||
async function setupDatabase() {
|
||||
const spinner = ora('Setting up database connection...').start();
|
||||
|
||||
try {
|
||||
client = new MongoClient(config.mongodb.uri);
|
||||
await client.connect();
|
||||
db = client.db('performance_test');
|
||||
|
||||
// Create collections
|
||||
collections.campaigns = db.collection('campaigns');
|
||||
collections.messages = db.collection('messages');
|
||||
collections.users = db.collection('users');
|
||||
collections.analytics = db.collection('analytics');
|
||||
|
||||
// Create indexes
|
||||
await collections.campaigns.createIndex({ tenantId: 1, createdAt: -1 });
|
||||
await collections.campaigns.createIndex({ 'targetAudience.groups': 1 });
|
||||
await collections.campaigns.createIndex({ status: 1 });
|
||||
|
||||
await collections.messages.createIndex({ tenantId: 1, campaignId: 1 });
|
||||
await collections.messages.createIndex({ status: 1, sentAt: -1 });
|
||||
|
||||
await collections.users.createIndex({ tenantId: 1, userId: 1 });
|
||||
await collections.users.createIndex({ groups: 1 });
|
||||
await collections.users.createIndex({ tags: 1 });
|
||||
|
||||
await collections.analytics.createIndex({ tenantId: 1, timestamp: -1 });
|
||||
await collections.analytics.createIndex({ eventType: 1, campaignId: 1 });
|
||||
|
||||
spinner.succeed('Database setup completed');
|
||||
} catch (error) {
|
||||
spinner.fail('Database setup failed');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupDatabase() {
|
||||
const spinner = ora('Cleaning up database...').start();
|
||||
|
||||
try {
|
||||
await db.dropDatabase();
|
||||
await client.close();
|
||||
spinner.succeed('Database cleanup completed');
|
||||
} catch (error) {
|
||||
spinner.fail('Database cleanup failed');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function createBenchmarkSuite() {
|
||||
const suite = new Benchmark.Suite('Database Operations');
|
||||
|
||||
// Insert benchmarks
|
||||
suite.add('Insert Single Campaign', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
const campaign = { ...testData.campaigns[0], _id: new Date().getTime() };
|
||||
await collections.campaigns.insertOne(campaign);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
suite.add('Bulk Insert Messages', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
const messages = testData.messages.slice(0, 100).map((m, i) => ({
|
||||
...m,
|
||||
_id: new Date().getTime() + i
|
||||
}));
|
||||
await collections.messages.insertMany(messages);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// Find benchmarks
|
||||
suite.add('Find Campaigns by Tenant', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.campaigns.find({
|
||||
tenantId: 'tenant-1'
|
||||
}).limit(100).toArray();
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
suite.add('Find Messages with Status', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.messages.find({
|
||||
status: 'sent',
|
||||
sentAt: { $gte: new Date(Date.now() - 86400000) }
|
||||
}).limit(100).toArray();
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
suite.add('Find Users by Groups', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.users.find({
|
||||
groups: { $in: ['group-1', 'group-2'] }
|
||||
}).limit(100).toArray();
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// Update benchmarks
|
||||
suite.add('Update Campaign Status', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.campaigns.updateOne(
|
||||
{ _id: testData.campaigns[0]._id },
|
||||
{ $set: { status: 'active', updatedAt: new Date() } }
|
||||
);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
suite.add('Bulk Update Message Status', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.messages.updateMany(
|
||||
{ status: 'pending', attempts: { $lt: 3 } },
|
||||
{ $set: { status: 'sent', sentAt: new Date() } }
|
||||
);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// Aggregation benchmarks
|
||||
suite.add('Campaign Analytics Aggregation', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.analytics.aggregate([
|
||||
{ $match: {
|
||||
campaignId: 'campaign-1',
|
||||
timestamp: { $gte: new Date(Date.now() - 86400000 * 7) }
|
||||
}},
|
||||
{ $group: {
|
||||
_id: '$eventType',
|
||||
count: { $sum: 1 }
|
||||
}},
|
||||
{ $sort: { count: -1 } }
|
||||
]).toArray();
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
suite.add('User Engagement Aggregation', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.analytics.aggregate([
|
||||
{ $match: {
|
||||
eventType: { $in: ['message_read', 'link_clicked'] }
|
||||
}},
|
||||
{ $group: {
|
||||
_id: '$userId',
|
||||
engagementCount: { $sum: 1 },
|
||||
lastEngagement: { $max: '$timestamp' }
|
||||
}},
|
||||
{ $sort: { engagementCount: -1 } },
|
||||
{ $limit: 100 }
|
||||
]).toArray();
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// Complex query benchmarks
|
||||
suite.add('Complex Campaign Query', {
|
||||
defer: true,
|
||||
fn: async function(deferred) {
|
||||
await collections.campaigns.find({
|
||||
tenantId: 'tenant-1',
|
||||
status: { $in: ['active', 'scheduled'] },
|
||||
'targetAudience.groups': { $in: ['group-1', 'group-2'] },
|
||||
'schedule.scheduledAt': { $lte: new Date() },
|
||||
'metrics.sent': { $gte: 100 }
|
||||
}).sort({ createdAt: -1 }).limit(50).toArray();
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
return suite;
|
||||
}
|
||||
|
||||
async function runBenchmarks() {
|
||||
console.log('\n' + chalk.bold.blue('Database Benchmark'));
|
||||
console.log('=' .repeat(80));
|
||||
|
||||
const results = [];
|
||||
|
||||
// Populate initial data
|
||||
const spinner = ora('Populating test data...').start();
|
||||
await collections.campaigns.insertMany(testData.campaigns);
|
||||
await collections.messages.insertMany(testData.messages.slice(0, 1000));
|
||||
await collections.users.insertMany(testData.users);
|
||||
await collections.analytics.insertMany(testData.analytics.slice(0, 5000));
|
||||
spinner.succeed('Test data populated');
|
||||
|
||||
const suite = createBenchmarkSuite();
|
||||
|
||||
suite.on('cycle', function(event) {
|
||||
const benchmark = event.target;
|
||||
console.log(chalk.yellow(benchmark.name));
|
||||
console.log(` ${chalk.green(benchmark.hz.toFixed(2))} ops/sec`);
|
||||
console.log(` ${chalk.cyan((1000 / benchmark.hz).toFixed(2))} ms/op`);
|
||||
|
||||
results.push({
|
||||
operation: benchmark.name,
|
||||
opsPerSec: benchmark.hz,
|
||||
msPerOp: 1000 / benchmark.hz,
|
||||
samples: benchmark.stats.sample.length
|
||||
});
|
||||
});
|
||||
|
||||
suite.on('complete', function() {
|
||||
console.log('\n' + chalk.green('✓ Database benchmarks completed'));
|
||||
});
|
||||
|
||||
// Run benchmarks
|
||||
await new Promise((resolve) => {
|
||||
suite.on('complete', resolve);
|
||||
suite.run({ async: true });
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
generateTestData();
|
||||
await setupDatabase();
|
||||
|
||||
const results = await runBenchmarks();
|
||||
|
||||
// Check against thresholds
|
||||
console.log('\n' + chalk.bold('Performance Analysis'));
|
||||
console.log('=' .repeat(80));
|
||||
|
||||
const thresholds = config.thresholds.database.operations;
|
||||
let allPassed = true;
|
||||
|
||||
results.forEach(result => {
|
||||
let threshold;
|
||||
if (result.operation.includes('Insert')) threshold = thresholds.insert;
|
||||
else if (result.operation.includes('Find')) threshold = thresholds.find;
|
||||
else if (result.operation.includes('Update')) threshold = thresholds.update;
|
||||
else if (result.operation.includes('Aggregation')) threshold = thresholds.aggregate;
|
||||
|
||||
if (threshold && result.opsPerSec < threshold) {
|
||||
console.log(chalk.red(`✗ ${result.operation}: ${result.opsPerSec.toFixed(2)} ops/sec (threshold: ${threshold})`));
|
||||
allPassed = false;
|
||||
} else {
|
||||
console.log(chalk.green(`✓ ${result.operation}: ${result.opsPerSec.toFixed(2)} ops/sec`));
|
||||
}
|
||||
});
|
||||
|
||||
if (allPassed) {
|
||||
console.log('\n' + chalk.green('✓ All database operations meet performance thresholds'));
|
||||
} else {
|
||||
console.log('\n' + chalk.red('✗ Some database operations are below performance thresholds'));
|
||||
}
|
||||
|
||||
// Save results
|
||||
await saveResults('database-benchmark', results);
|
||||
|
||||
await cleanupDatabase();
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Error:'), error);
|
||||
if (client) await client.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
151
marketing-agent/performance-tests/src/tests/load-test.js
Normal file
151
marketing-agent/performance-tests/src/tests/load-test.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
import { config } from '../config.js';
|
||||
|
||||
// Custom metrics
|
||||
const errorRate = new Rate('errors');
|
||||
const campaignCreationTime = new Trend('campaign_creation_time');
|
||||
const messageDeliveryTime = new Trend('message_delivery_time');
|
||||
const analyticsQueryTime = new Trend('analytics_query_time');
|
||||
|
||||
// Test configuration
|
||||
export const options = {
|
||||
stages: config.test.load.stages,
|
||||
thresholds: config.test.load.thresholds
|
||||
};
|
||||
|
||||
// Setup function - runs once before the test
|
||||
export function setup() {
|
||||
// Login and get auth token
|
||||
const loginRes = http.post(`${config.apiGateway.url}/api/v1/auth/login`, {
|
||||
email: 'test@example.com',
|
||||
password: 'testpass123'
|
||||
});
|
||||
|
||||
const authToken = loginRes.json('token');
|
||||
return { authToken };
|
||||
}
|
||||
|
||||
// Main test function - runs for each virtual user
|
||||
export default function(data) {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${data.authToken}`,
|
||||
'X-API-Key': config.apiGateway.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// Scenario 1: Create a campaign
|
||||
const campaignPayload = {
|
||||
name: `Load Test Campaign ${__VU}-${__ITER}`,
|
||||
description: 'Campaign created during load testing',
|
||||
targetAudience: {
|
||||
groups: [`group-${__VU % 10}`],
|
||||
tags: ['load-test', `vu-${__VU}`]
|
||||
},
|
||||
message: {
|
||||
content: `This is message ${__ITER} from VU ${__VU}`,
|
||||
type: 'text'
|
||||
},
|
||||
schedule: {
|
||||
type: 'immediate'
|
||||
}
|
||||
};
|
||||
|
||||
const createCampaignRes = http.post(
|
||||
`${config.apiGateway.url}/api/v1/orchestrator/campaigns`,
|
||||
JSON.stringify(campaignPayload),
|
||||
{ headers, tags: { name: 'CreateCampaign' } }
|
||||
);
|
||||
|
||||
check(createCampaignRes, {
|
||||
'campaign created': (r) => r.status === 201,
|
||||
'has campaign ID': (r) => r.json('campaign.id') !== undefined
|
||||
});
|
||||
|
||||
errorRate.add(createCampaignRes.status !== 201);
|
||||
campaignCreationTime.add(createCampaignRes.timings.duration);
|
||||
|
||||
sleep(1);
|
||||
|
||||
// Scenario 2: Send a message
|
||||
if (createCampaignRes.status === 201) {
|
||||
const campaignId = createCampaignRes.json('campaign.id');
|
||||
|
||||
const sendMessageRes = http.post(
|
||||
`${config.apiGateway.url}/api/v1/orchestrator/campaigns/${campaignId}/execute`,
|
||||
null,
|
||||
{ headers, tags: { name: 'SendMessage' } }
|
||||
);
|
||||
|
||||
check(sendMessageRes, {
|
||||
'message sent': (r) => r.status === 200
|
||||
});
|
||||
|
||||
errorRate.add(sendMessageRes.status !== 200);
|
||||
messageDeliveryTime.add(sendMessageRes.timings.duration);
|
||||
}
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Scenario 3: Query analytics
|
||||
const analyticsRes = http.get(
|
||||
`${config.apiGateway.url}/api/v1/analytics/campaigns/overview?period=daily`,
|
||||
{ headers, tags: { name: 'GetAnalytics' } }
|
||||
);
|
||||
|
||||
check(analyticsRes, {
|
||||
'analytics retrieved': (r) => r.status === 200,
|
||||
'has data': (r) => r.json('data') !== undefined
|
||||
});
|
||||
|
||||
errorRate.add(analyticsRes.status !== 200);
|
||||
analyticsQueryTime.add(analyticsRes.timings.duration);
|
||||
|
||||
sleep(1);
|
||||
|
||||
// Scenario 4: List campaigns
|
||||
const listCampaignsRes = http.get(
|
||||
`${config.apiGateway.url}/api/v1/orchestrator/campaigns?limit=10`,
|
||||
{ headers, tags: { name: 'ListCampaigns' } }
|
||||
);
|
||||
|
||||
check(listCampaignsRes, {
|
||||
'campaigns listed': (r) => r.status === 200,
|
||||
'has campaigns array': (r) => Array.isArray(r.json('campaigns'))
|
||||
});
|
||||
|
||||
errorRate.add(listCampaignsRes.status !== 200);
|
||||
|
||||
sleep(2);
|
||||
|
||||
// Scenario 5: Check compliance
|
||||
const compliancePayload = {
|
||||
content: `Test message ${__ITER} for compliance check`,
|
||||
context: {
|
||||
campaignId: 'test-campaign',
|
||||
targetGroups: ['test-group']
|
||||
}
|
||||
};
|
||||
|
||||
const complianceRes = http.post(
|
||||
`${config.apiGateway.url}/api/v1/safety/check`,
|
||||
JSON.stringify(compliancePayload),
|
||||
{ headers, tags: { name: 'CheckCompliance' } }
|
||||
);
|
||||
|
||||
check(complianceRes, {
|
||||
'compliance checked': (r) => r.status === 200,
|
||||
'has approval status': (r) => r.json('approved') !== undefined
|
||||
});
|
||||
|
||||
errorRate.add(complianceRes.status !== 200);
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
// Teardown function - runs once after the test
|
||||
export function teardown(data) {
|
||||
// Clean up test data if needed
|
||||
console.log('Load test completed');
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import axios from 'axios';
|
||||
import { io } from 'socket.io-client';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { config } from '../config.js';
|
||||
import { saveResults } from '../utils/results.js';
|
||||
|
||||
class MessageThroughputTest {
|
||||
constructor() {
|
||||
this.results = {
|
||||
totalMessages: 0,
|
||||
successfulMessages: 0,
|
||||
failedMessages: 0,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
latencies: [],
|
||||
throughputSamples: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
this.sockets = [];
|
||||
this.messageQueue = [];
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
async setup() {
|
||||
const spinner = ora('Setting up test environment...').start();
|
||||
|
||||
try {
|
||||
// Login and get auth token
|
||||
const loginRes = await axios.post(`${config.apiGateway.url}/api/v1/auth/login`, {
|
||||
email: 'test@example.com',
|
||||
password: 'testpass123'
|
||||
});
|
||||
|
||||
this.authToken = loginRes.data.token;
|
||||
|
||||
// Create test campaign
|
||||
const campaignRes = await axios.post(
|
||||
`${config.apiGateway.url}/api/v1/orchestrator/campaigns`,
|
||||
{
|
||||
name: 'Message Throughput Test Campaign',
|
||||
description: 'Campaign for throughput testing',
|
||||
targetAudience: {
|
||||
groups: ['throughput-test-group'],
|
||||
tags: ['performance-test']
|
||||
},
|
||||
message: {
|
||||
content: 'Throughput test message',
|
||||
type: 'text'
|
||||
},
|
||||
schedule: {
|
||||
type: 'immediate'
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.authToken}`,
|
||||
'X-API-Key': config.apiGateway.apiKey
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.campaignId = campaignRes.data.campaign.id;
|
||||
|
||||
// Connect WebSocket clients for real-time monitoring
|
||||
for (let i = 0; i < config.test.message.concurrentSenders; i++) {
|
||||
const socket = io(config.apiGateway.url, {
|
||||
auth: {
|
||||
token: this.authToken
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('message:sent', (data) => {
|
||||
this.onMessageSent(data);
|
||||
});
|
||||
|
||||
socket.on('message:delivered', (data) => {
|
||||
this.onMessageDelivered(data);
|
||||
});
|
||||
|
||||
socket.on('message:failed', (data) => {
|
||||
this.onMessageFailed(data);
|
||||
});
|
||||
|
||||
this.sockets.push(socket);
|
||||
}
|
||||
|
||||
spinner.succeed('Test environment setup completed');
|
||||
} catch (error) {
|
||||
spinner.fail('Setup failed');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async generateMessages() {
|
||||
const totalMessages = config.test.message.totalMessages;
|
||||
const messageSize = config.test.message.messageSize;
|
||||
|
||||
for (let i = 0; i < totalMessages; i++) {
|
||||
this.messageQueue.push({
|
||||
id: `msg-${i}`,
|
||||
recipient: {
|
||||
userId: `user-${i % 1000}`,
|
||||
chatId: `chat-${i % 100}`,
|
||||
username: `user${i % 1000}`
|
||||
},
|
||||
content: `Performance test message ${i} `.padEnd(messageSize, 'X'),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async runTest() {
|
||||
console.log('\n' + chalk.bold.green('Starting Message Throughput Test'));
|
||||
console.log(`Total messages: ${config.test.message.totalMessages}`);
|
||||
console.log(`Concurrent senders: ${config.test.message.concurrentSenders}`);
|
||||
console.log(`Message size: ${config.test.message.messageSize} bytes\n`);
|
||||
|
||||
this.results.startTime = Date.now();
|
||||
this.running = true;
|
||||
|
||||
// Start throughput monitoring
|
||||
this.monitorThroughput();
|
||||
|
||||
// Process messages in batches
|
||||
const batchSize = config.test.message.batchSize;
|
||||
const concurrentSenders = config.test.message.concurrentSenders;
|
||||
|
||||
const progressBar = ora('Sending messages...').start();
|
||||
|
||||
while (this.messageQueue.length > 0) {
|
||||
const batch = this.messageQueue.splice(0, batchSize * concurrentSenders);
|
||||
const promises = [];
|
||||
|
||||
// Distribute batch across concurrent senders
|
||||
for (let i = 0; i < concurrentSenders && i < batch.length; i++) {
|
||||
const senderBatch = batch.filter((_, index) => index % concurrentSenders === i);
|
||||
promises.push(this.sendBatch(senderBatch));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const progress = ((this.results.totalMessages / config.test.message.totalMessages) * 100).toFixed(1);
|
||||
progressBar.text = `Sending messages... ${progress}% (${this.results.totalMessages}/${config.test.message.totalMessages})`;
|
||||
}
|
||||
|
||||
progressBar.succeed('All messages sent');
|
||||
|
||||
// Wait for all messages to be processed
|
||||
console.log(chalk.yellow('Waiting for message delivery confirmation...'));
|
||||
await this.waitForCompletion();
|
||||
|
||||
this.running = false;
|
||||
this.results.endTime = Date.now();
|
||||
}
|
||||
|
||||
async sendBatch(messages) {
|
||||
const promises = messages.map(async (message) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${config.apiGateway.url}/api/v1/gramjs-adapter/messages/send`,
|
||||
{
|
||||
campaignId: this.campaignId,
|
||||
recipients: [message.recipient],
|
||||
message: {
|
||||
content: message.content,
|
||||
type: 'text'
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.authToken}`,
|
||||
'X-API-Key': config.apiGateway.apiKey
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const latency = Date.now() - startTime;
|
||||
this.results.latencies.push(latency);
|
||||
this.results.totalMessages++;
|
||||
|
||||
if (response.data.success) {
|
||||
this.results.successfulMessages++;
|
||||
} else {
|
||||
this.results.failedMessages++;
|
||||
this.results.errors.push({
|
||||
message: message.id,
|
||||
error: response.data.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const latency = Date.now() - startTime;
|
||||
this.results.latencies.push(latency);
|
||||
this.results.totalMessages++;
|
||||
this.results.failedMessages++;
|
||||
this.results.errors.push({
|
||||
message: message.id,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
monitorThroughput() {
|
||||
let lastCount = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (!this.running) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCount = this.results.successfulMessages;
|
||||
const throughput = currentCount - lastCount;
|
||||
this.results.throughputSamples.push({
|
||||
timestamp: Date.now(),
|
||||
throughput
|
||||
});
|
||||
|
||||
lastCount = currentCount;
|
||||
}, 1000); // Sample every second
|
||||
}
|
||||
|
||||
async waitForCompletion() {
|
||||
const timeout = 60000; // 1 minute timeout
|
||||
const startWait = Date.now();
|
||||
|
||||
while (Date.now() - startWait < timeout) {
|
||||
const pending = this.results.totalMessages -
|
||||
(this.results.successfulMessages + this.results.failedMessages);
|
||||
|
||||
if (pending === 0) break;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
onMessageSent(data) {
|
||||
// Real-time event handling
|
||||
}
|
||||
|
||||
onMessageDelivered(data) {
|
||||
// Real-time event handling
|
||||
}
|
||||
|
||||
onMessageFailed(data) {
|
||||
// Real-time event handling
|
||||
}
|
||||
|
||||
analyzeResults() {
|
||||
console.log('\n' + chalk.bold.cyan('Message Throughput Test Results'));
|
||||
console.log('=' .repeat(80));
|
||||
|
||||
const duration = (this.results.endTime - this.results.startTime) / 1000;
|
||||
const averageThroughput = this.results.successfulMessages / duration;
|
||||
|
||||
// Calculate latency percentiles
|
||||
const sortedLatencies = [...this.results.latencies].sort((a, b) => a - b);
|
||||
const p50 = sortedLatencies[Math.floor(sortedLatencies.length * 0.5)];
|
||||
const p90 = sortedLatencies[Math.floor(sortedLatencies.length * 0.9)];
|
||||
const p95 = sortedLatencies[Math.floor(sortedLatencies.length * 0.95)];
|
||||
const p99 = sortedLatencies[Math.floor(sortedLatencies.length * 0.99)];
|
||||
|
||||
// Calculate throughput statistics
|
||||
const throughputValues = this.results.throughputSamples.map(s => s.throughput);
|
||||
const maxThroughput = Math.max(...throughputValues);
|
||||
const minThroughput = Math.min(...throughputValues);
|
||||
|
||||
console.log(`Duration: ${chalk.yellow(duration.toFixed(2))} seconds`);
|
||||
console.log(`Total messages: ${chalk.green(this.results.totalMessages)}`);
|
||||
console.log(`Successful: ${chalk.green(this.results.successfulMessages)}`);
|
||||
console.log(`Failed: ${this.results.failedMessages > 0 ? chalk.red(this.results.failedMessages) : chalk.green(this.results.failedMessages)}`);
|
||||
console.log(`Success rate: ${chalk.cyan(((this.results.successfulMessages / this.results.totalMessages) * 100).toFixed(2))}%`);
|
||||
|
||||
console.log('\nThroughput:');
|
||||
console.log(` Average: ${chalk.green(averageThroughput.toFixed(2))} msg/sec`);
|
||||
console.log(` Maximum: ${chalk.green(maxThroughput)} msg/sec`);
|
||||
console.log(` Minimum: ${chalk.yellow(minThroughput)} msg/sec`);
|
||||
|
||||
console.log('\nLatency:');
|
||||
console.log(` p50: ${chalk.cyan(p50)} ms`);
|
||||
console.log(` p90: ${chalk.cyan(p90)} ms`);
|
||||
console.log(` p95: ${chalk.cyan(p95)} ms`);
|
||||
console.log(` p99: ${chalk.cyan(p99)} ms`);
|
||||
|
||||
// Check against thresholds
|
||||
const thresholds = config.thresholds.message;
|
||||
|
||||
if (averageThroughput < thresholds.throughput) {
|
||||
console.log(chalk.red(`\n⚠️ Average throughput (${averageThroughput.toFixed(2)} msg/sec) is below threshold (${thresholds.throughput} msg/sec)`));
|
||||
} else {
|
||||
console.log(chalk.green(`\n✓ Average throughput meets threshold`));
|
||||
}
|
||||
|
||||
if (p95 > thresholds.latency) {
|
||||
console.log(chalk.red(`⚠️ P95 latency (${p95}ms) exceeds threshold (${thresholds.latency}ms)`));
|
||||
} else {
|
||||
console.log(chalk.green(`✓ P95 latency meets threshold`));
|
||||
}
|
||||
|
||||
return {
|
||||
duration,
|
||||
totalMessages: this.results.totalMessages,
|
||||
successfulMessages: this.results.successfulMessages,
|
||||
failedMessages: this.results.failedMessages,
|
||||
successRate: (this.results.successfulMessages / this.results.totalMessages) * 100,
|
||||
throughput: {
|
||||
average: averageThroughput,
|
||||
max: maxThroughput,
|
||||
min: minThroughput
|
||||
},
|
||||
latency: {
|
||||
p50,
|
||||
p90,
|
||||
p95,
|
||||
p99
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
const spinner = ora('Cleaning up...').start();
|
||||
|
||||
// Close WebSocket connections
|
||||
this.sockets.forEach(socket => socket.close());
|
||||
|
||||
// Delete test campaign
|
||||
try {
|
||||
await axios.delete(
|
||||
`${config.apiGateway.url}/api/v1/orchestrator/campaigns/${this.campaignId}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.authToken}`,
|
||||
'X-API-Key': config.apiGateway.apiKey
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
spinner.succeed('Cleanup completed');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const test = new MessageThroughputTest();
|
||||
|
||||
try {
|
||||
await test.setup();
|
||||
await test.generateMessages();
|
||||
await test.runTest();
|
||||
|
||||
const results = test.analyzeResults();
|
||||
await saveResults('message-throughput', results);
|
||||
|
||||
console.log('\n' + chalk.green('✓ Message throughput test completed'));
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Error:'), error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await test.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
262
marketing-agent/performance-tests/src/tests/stress-test.js
Normal file
262
marketing-agent/performance-tests/src/tests/stress-test.js
Normal file
@@ -0,0 +1,262 @@
|
||||
import autocannon from 'autocannon';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { config } from '../config.js';
|
||||
import { saveResults } from '../utils/results.js';
|
||||
|
||||
const stressScenarios = [
|
||||
{
|
||||
name: 'Burst Traffic',
|
||||
description: 'Sudden spike in traffic',
|
||||
connections: 1000,
|
||||
duration: 30,
|
||||
amount: 50000, // total requests
|
||||
workers: 4
|
||||
},
|
||||
{
|
||||
name: 'Sustained High Load',
|
||||
description: 'Continuous high traffic',
|
||||
connections: 500,
|
||||
duration: 120, // 2 minutes
|
||||
workers: 4
|
||||
},
|
||||
{
|
||||
name: 'Connection Saturation',
|
||||
description: 'Maximum concurrent connections',
|
||||
connections: 2000,
|
||||
duration: 60,
|
||||
workers: 8
|
||||
},
|
||||
{
|
||||
name: 'Large Payload Stress',
|
||||
description: 'Large message payloads',
|
||||
connections: 100,
|
||||
duration: 60,
|
||||
workers: 2,
|
||||
bodySize: 1024 * 1024 // 1MB payload
|
||||
}
|
||||
];
|
||||
|
||||
function generateLargePayload(size) {
|
||||
return {
|
||||
name: 'Stress Test Campaign',
|
||||
description: 'A'.repeat(size / 2),
|
||||
targetAudience: {
|
||||
groups: Array(100).fill('group'),
|
||||
tags: Array(1000).fill('tag')
|
||||
},
|
||||
message: {
|
||||
content: 'B'.repeat(size / 2),
|
||||
type: 'text'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function runStressScenario(scenario) {
|
||||
const spinner = ora(`Running ${scenario.name}...`).start();
|
||||
|
||||
const options = {
|
||||
url: `${config.apiGateway.url}/api/v1/orchestrator/campaigns`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-token',
|
||||
'X-API-Key': config.apiGateway.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
connections: scenario.connections,
|
||||
duration: scenario.duration,
|
||||
workers: scenario.workers
|
||||
};
|
||||
|
||||
if (scenario.amount) {
|
||||
options.amount = scenario.amount;
|
||||
}
|
||||
|
||||
if (scenario.bodySize) {
|
||||
options.body = JSON.stringify(generateLargePayload(scenario.bodySize));
|
||||
} else {
|
||||
options.body = JSON.stringify({
|
||||
name: 'Stress Test Campaign',
|
||||
description: 'Auto-generated campaign for stress testing',
|
||||
targetAudience: {
|
||||
groups: ['stress-test-group'],
|
||||
tags: ['stress-test']
|
||||
},
|
||||
message: {
|
||||
content: 'This is a stress test message',
|
||||
type: 'text'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const instance = autocannon(options);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const metrics = {
|
||||
scenario: scenario.name,
|
||||
description: scenario.description,
|
||||
startTime: Date.now(),
|
||||
systemMetrics: []
|
||||
};
|
||||
|
||||
// Collect system metrics during test
|
||||
const metricsInterval = setInterval(() => {
|
||||
metrics.systemMetrics.push({
|
||||
timestamp: Date.now(),
|
||||
memory: process.memoryUsage(),
|
||||
cpu: process.cpuUsage()
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
instance.on('done', (results) => {
|
||||
clearInterval(metricsInterval);
|
||||
metrics.endTime = Date.now();
|
||||
metrics.duration = metrics.endTime - metrics.startTime;
|
||||
metrics.results = results;
|
||||
|
||||
spinner.succeed(`${scenario.name} completed`);
|
||||
resolve(metrics);
|
||||
});
|
||||
|
||||
instance.on('error', (err) => {
|
||||
clearInterval(metricsInterval);
|
||||
spinner.fail(`${scenario.name} failed: ${err.message}`);
|
||||
metrics.error = err.message;
|
||||
resolve(metrics);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function analyzeStressResults(results) {
|
||||
console.log('\n' + chalk.bold.red('Stress Test Results'));
|
||||
console.log('=' .repeat(80));
|
||||
|
||||
const summary = [];
|
||||
|
||||
results.forEach((result) => {
|
||||
const { scenario, description, results: data, error } = result;
|
||||
|
||||
console.log(`\n${chalk.yellow(scenario)}`);
|
||||
console.log(` ${chalk.gray(description)}`);
|
||||
|
||||
if (error) {
|
||||
console.log(chalk.red(` ❌ Test failed: ${error}`));
|
||||
summary.push({ scenario, status: 'failed', error });
|
||||
return;
|
||||
}
|
||||
|
||||
const analysis = {
|
||||
scenario,
|
||||
status: 'completed',
|
||||
requests: data.requests.total,
|
||||
throughput: data.throughput.mean,
|
||||
latency: {
|
||||
p50: data.latency.p50,
|
||||
p90: data.latency.p90,
|
||||
p95: data.latency.p95,
|
||||
p99: data.latency.p99,
|
||||
max: data.latency.max
|
||||
},
|
||||
errors: data.errors,
|
||||
timeouts: data.timeouts,
|
||||
connections: data.connections,
|
||||
systemLoad: calculateSystemLoad(result.systemMetrics)
|
||||
};
|
||||
|
||||
summary.push(analysis);
|
||||
|
||||
console.log(` Requests: ${chalk.green(data.requests.total)}`);
|
||||
console.log(` Throughput: ${chalk.green(data.throughput.mean.toFixed(2))} req/sec`);
|
||||
console.log(` Errors: ${data.errors > 0 ? chalk.red(data.errors) : chalk.green(data.errors)}`);
|
||||
console.log(` Timeouts: ${data.timeouts > 0 ? chalk.red(data.timeouts) : chalk.green(data.timeouts)}`);
|
||||
console.log(` Latency:`);
|
||||
console.log(` p50: ${chalk.cyan(data.latency.p50)} ms`);
|
||||
console.log(` p90: ${chalk.cyan(data.latency.p90)} ms`);
|
||||
console.log(` p95: ${chalk.cyan(data.latency.p95)} ms`);
|
||||
console.log(` p99: ${chalk.cyan(data.latency.p99)} ms`);
|
||||
console.log(` max: ${chalk.cyan(data.latency.max)} ms`);
|
||||
|
||||
// Performance degradation analysis
|
||||
if (data.latency.p95 > 1000) {
|
||||
console.log(chalk.red(` ⚠️ Severe performance degradation detected`));
|
||||
} else if (data.latency.p95 > 500) {
|
||||
console.log(chalk.yellow(` ⚠️ Moderate performance degradation detected`));
|
||||
}
|
||||
|
||||
const errorRate = (data.errors / data.requests.total) * 100;
|
||||
if (errorRate > 5) {
|
||||
console.log(chalk.red(` ⚠️ High error rate: ${errorRate.toFixed(2)}%`));
|
||||
}
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
function calculateSystemLoad(metrics) {
|
||||
if (!metrics || metrics.length === 0) return null;
|
||||
|
||||
const lastMetric = metrics[metrics.length - 1];
|
||||
const firstMetric = metrics[0];
|
||||
|
||||
return {
|
||||
memoryUsage: {
|
||||
heapUsed: lastMetric.memory.heapUsed,
|
||||
heapTotal: lastMetric.memory.heapTotal,
|
||||
external: lastMetric.memory.external,
|
||||
rss: lastMetric.memory.rss
|
||||
},
|
||||
cpuUsage: {
|
||||
user: lastMetric.cpu.user - firstMetric.cpu.user,
|
||||
system: lastMetric.cpu.system - firstMetric.cpu.system
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.red('Starting Stress Tests'));
|
||||
console.log(chalk.yellow('⚠️ Warning: These tests will put significant load on the system'));
|
||||
console.log(`Testing against: ${config.apiGateway.url}\n`);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const scenario of stressScenarios) {
|
||||
const result = await runStressScenario(scenario);
|
||||
results.push(result);
|
||||
|
||||
// Cool down period between scenarios
|
||||
console.log(chalk.gray(' Cooling down...'));
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
}
|
||||
|
||||
const summary = await analyzeStressResults(results);
|
||||
|
||||
// Save results
|
||||
await saveResults('stress-test', summary);
|
||||
|
||||
// Overall system assessment
|
||||
console.log('\n' + chalk.bold('System Stress Assessment'));
|
||||
console.log('=' .repeat(80));
|
||||
|
||||
const failedScenarios = summary.filter(s => s.status === 'failed');
|
||||
const highErrorScenarios = summary.filter(s =>
|
||||
s.status === 'completed' && s.errors > 0 && (s.errors / s.requests) > 0.05
|
||||
);
|
||||
|
||||
if (failedScenarios.length === 0 && highErrorScenarios.length === 0) {
|
||||
console.log(chalk.green('✓ System handled all stress scenarios successfully'));
|
||||
} else {
|
||||
console.log(chalk.red(`✗ System showed signs of stress in ${failedScenarios.length + highErrorScenarios.length} scenarios`));
|
||||
|
||||
if (failedScenarios.length > 0) {
|
||||
console.log(chalk.red(` - ${failedScenarios.length} scenarios failed completely`));
|
||||
}
|
||||
|
||||
if (highErrorScenarios.length > 0) {
|
||||
console.log(chalk.yellow(` - ${highErrorScenarios.length} scenarios had high error rates`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + chalk.green('✓ Stress tests completed'));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
247
marketing-agent/performance-tests/src/utils/results.js
Normal file
247
marketing-agent/performance-tests/src/utils/results.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { config } from '../config.js';
|
||||
|
||||
export async function saveResults(testName, results) {
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-');
|
||||
const outputDir = config.report.outputDir;
|
||||
|
||||
// Ensure output directory exists
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
// Save JSON format
|
||||
if (config.report.formats.includes('json')) {
|
||||
const jsonPath = path.join(outputDir, `${testName}-${timestamp}.json`);
|
||||
await fs.writeFile(jsonPath, JSON.stringify(results, null, 2));
|
||||
}
|
||||
|
||||
// Save CSV format
|
||||
if (config.report.formats.includes('csv')) {
|
||||
const csvPath = path.join(outputDir, `${testName}-${timestamp}.csv`);
|
||||
const csv = convertToCSV(results);
|
||||
await fs.writeFile(csvPath, csv);
|
||||
}
|
||||
|
||||
// Save HTML format
|
||||
if (config.report.formats.includes('html')) {
|
||||
const htmlPath = path.join(outputDir, `${testName}-${timestamp}.html`);
|
||||
const html = generateHTMLReport(testName, results);
|
||||
await fs.writeFile(htmlPath, html);
|
||||
}
|
||||
}
|
||||
|
||||
function convertToCSV(results) {
|
||||
if (Array.isArray(results)) {
|
||||
if (results.length === 0) return '';
|
||||
|
||||
const headers = Object.keys(results[0]).join(',');
|
||||
const rows = results.map(row =>
|
||||
Object.values(row).map(value =>
|
||||
typeof value === 'object' ? JSON.stringify(value) : value
|
||||
).join(',')
|
||||
);
|
||||
|
||||
return [headers, ...rows].join('\n');
|
||||
} else {
|
||||
// Flatten object for CSV
|
||||
const flattened = flattenObject(results);
|
||||
const headers = Object.keys(flattened).join(',');
|
||||
const values = Object.values(flattened).join(',');
|
||||
return [headers, values].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function flattenObject(obj, prefix = '') {
|
||||
const flattened = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
Object.assign(flattened, flattenObject(value, newKey));
|
||||
} else {
|
||||
flattened[newKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function generateHTMLReport(testName, results) {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${testName} Performance Report</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.metadata {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.metric {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
padding: 15px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.metric-label {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.success {
|
||||
color: #28a745;
|
||||
}
|
||||
.warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
.chart {
|
||||
margin: 20px 0;
|
||||
}
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>${testName} Performance Report</h1>
|
||||
<div class="metadata">
|
||||
Generated: ${timestamp}
|
||||
</div>
|
||||
|
||||
${generateHTMLContent(results)}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function generateHTMLContent(results) {
|
||||
if (Array.isArray(results)) {
|
||||
// Generate table for array results
|
||||
if (results.length === 0) return '<p>No results</p>';
|
||||
|
||||
const headers = Object.keys(results[0]);
|
||||
const tableHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
${headers.map(h => `<th>${h}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${results.map(row => `
|
||||
<tr>
|
||||
${headers.map(h => `<td>${formatValue(row[h])}</td>`).join('')}
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
return tableHTML;
|
||||
} else {
|
||||
// Generate metrics display for object results
|
||||
return generateMetricsHTML(results);
|
||||
}
|
||||
}
|
||||
|
||||
function generateMetricsHTML(results, prefix = '') {
|
||||
let html = '';
|
||||
|
||||
for (const [key, value] of Object.entries(results)) {
|
||||
const displayKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
html += `<h3>${displayKey}</h3>`;
|
||||
html += generateMetricsHTML(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
html += `<h3>${displayKey}</h3>`;
|
||||
html += `<pre>${JSON.stringify(value, null, 2)}</pre>`;
|
||||
} else {
|
||||
html += `
|
||||
<div class="metric">
|
||||
<div class="metric-label">${displayKey}</div>
|
||||
<div class="metric-value ${getValueClass(key, value)}">${formatValue(value)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (typeof value === 'number') {
|
||||
if (value % 1 === 0) {
|
||||
return value.toLocaleString();
|
||||
} else {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function getValueClass(key, value) {
|
||||
key = key.toLowerCase();
|
||||
|
||||
if (key.includes('error') || key.includes('fail')) {
|
||||
return value > 0 ? 'error' : 'success';
|
||||
} else if (key.includes('success') || key.includes('throughput')) {
|
||||
return 'success';
|
||||
} else if (key.includes('latency') || key.includes('duration')) {
|
||||
return value > 1000 ? 'warning' : 'success';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
Reference in New Issue
Block a user