338 lines
12 KiB
JavaScript
338 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080';
|
|
const RESULTS_FILE = path.join(__dirname, '../performance-test-results.json');
|
|
|
|
class PerformanceTester {
|
|
constructor(baseUrl) {
|
|
this.baseUrl = baseUrl;
|
|
this.results = [];
|
|
}
|
|
|
|
async testEndpoint(endpoint, method = 'GET', body = null) {
|
|
const url = `${this.baseUrl}${endpoint}`;
|
|
const startTime = Date.now();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
method: method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
};
|
|
|
|
if (body) {
|
|
options.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(body));
|
|
}
|
|
|
|
const protocol = url.startsWith('https') ? https : http;
|
|
|
|
const req = protocol.request(url, options, (res) => {
|
|
let data = '';
|
|
|
|
res.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
|
|
res.on('end', () => {
|
|
const endTime = Date.now();
|
|
const duration = endTime - startTime;
|
|
|
|
resolve({
|
|
endpoint,
|
|
method,
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
success: res.statusCode >= 200 && res.statusCode < 300,
|
|
dataSize: data.length
|
|
});
|
|
});
|
|
});
|
|
|
|
req.on('error', (error) => {
|
|
const endTime = Date.now();
|
|
const duration = endTime - startTime;
|
|
|
|
resolve({
|
|
endpoint,
|
|
method,
|
|
statusCode: 0,
|
|
duration,
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
});
|
|
|
|
if (body) {
|
|
req.write(JSON.stringify(body));
|
|
}
|
|
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async runLoadTest(endpoint, concurrentRequests = 10, totalRequests = 100) {
|
|
console.log(`\n📊 开始负载测试: ${endpoint}`);
|
|
console.log(` 并发数: ${concurrentRequests}`);
|
|
console.log(` 总请求数: ${totalRequests}\n`);
|
|
|
|
const results = [];
|
|
const startTime = Date.now();
|
|
|
|
for (let i = 0; i < totalRequests; i += concurrentRequests) {
|
|
const batch = Math.min(concurrentRequests, totalRequests - i);
|
|
const promises = [];
|
|
|
|
for (let j = 0; j < batch; j++) {
|
|
promises.push(this.testEndpoint(endpoint));
|
|
}
|
|
|
|
const batchResults = await Promise.all(promises);
|
|
results.push(...batchResults);
|
|
|
|
console.log(` 进度: ${Math.min(i + batch, totalRequests)}/${totalRequests} 请求已完成`);
|
|
}
|
|
|
|
const endTime = Date.now();
|
|
const totalDuration = endTime - startTime;
|
|
|
|
const successfulRequests = results.filter(r => r.success);
|
|
const failedRequests = results.filter(r => !r.success);
|
|
|
|
const durations = successfulRequests.map(r => r.duration);
|
|
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
const minDuration = durations.length > 0 ? Math.min(...durations) : 0;
|
|
const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
|
|
const p95Duration = this.calculatePercentile(durations, 95);
|
|
const p99Duration = this.calculatePercentile(durations, 99);
|
|
|
|
const throughput = (successfulRequests.length / totalDuration) * 1000;
|
|
|
|
return {
|
|
endpoint,
|
|
concurrentRequests,
|
|
totalRequests,
|
|
successfulRequests: successfulRequests.length,
|
|
failedRequests: failedRequests.length,
|
|
successRate: (successfulRequests.length / totalRequests * 100).toFixed(2),
|
|
totalDuration,
|
|
avgDuration,
|
|
minDuration,
|
|
maxDuration,
|
|
p95Duration,
|
|
p99Duration,
|
|
throughput: throughput.toFixed(2),
|
|
results
|
|
};
|
|
}
|
|
|
|
calculatePercentile(values, percentile) {
|
|
if (values.length === 0) return 0;
|
|
|
|
const sorted = [...values].sort((a, b) => a - b);
|
|
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
|
|
return sorted[Math.max(0, index)];
|
|
}
|
|
|
|
async runPerformanceTests() {
|
|
console.log('🚀 开始性能测试...\n');
|
|
|
|
const endpoints = [
|
|
{ path: '/api/auth/login', method: 'POST', body: { username: 'admin', password: 'admin123' } },
|
|
{ path: '/api/users', method: 'GET' },
|
|
{ path: '/api/roles', method: 'GET' },
|
|
{ path: '/api/menus', method: 'GET' },
|
|
{ path: '/api/dicts', method: 'GET' },
|
|
];
|
|
|
|
for (const endpoint of endpoints) {
|
|
console.log(`\n📡 测试端点: ${endpoint.method} ${endpoint.path}`);
|
|
|
|
const results = [];
|
|
const iterations = 10;
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
const result = await this.testEndpoint(endpoint.path, endpoint.method, endpoint.body);
|
|
results.push(result);
|
|
console.log(` ${i + 1}/${iterations}: ${result.duration}ms - ${result.success ? '✅' : '❌'}`);
|
|
}
|
|
|
|
const durations = results.filter(r => r.success).map(r => r.duration);
|
|
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
const minDuration = durations.length > 0 ? Math.min(...durations) : 0;
|
|
const maxDuration = durations.length > 0 ? Math.max(...durations) : 0;
|
|
const successRate = (results.filter(r => r.success).length / results.length * 100).toFixed(2);
|
|
|
|
this.results.push({
|
|
endpoint: endpoint.path,
|
|
method: endpoint.method,
|
|
avgDuration,
|
|
minDuration,
|
|
maxDuration,
|
|
successRate,
|
|
status: this.evaluatePerformance(avgDuration)
|
|
});
|
|
}
|
|
|
|
this.saveResults();
|
|
this.printSummary();
|
|
}
|
|
|
|
evaluatePerformance(avgDuration) {
|
|
if (avgDuration < 100) {
|
|
return '🟢 优秀';
|
|
} else if (avgDuration < 300) {
|
|
return '🟡 良好';
|
|
} else if (avgDuration < 500) {
|
|
return '🟠 一般';
|
|
} else {
|
|
return '🔴 需要优化';
|
|
}
|
|
}
|
|
|
|
saveResults() {
|
|
const timestamp = new Date().toISOString();
|
|
const data = {
|
|
timestamp,
|
|
performanceTests: this.results,
|
|
loadTests: this.loadTestResults
|
|
};
|
|
|
|
const history = [];
|
|
if (fs.existsSync(RESULTS_FILE)) {
|
|
try {
|
|
history.push(...JSON.parse(fs.readFileSync(RESULTS_FILE, 'utf8')));
|
|
} catch (e) {
|
|
console.warn('⚠️ 无法解析历史结果文件');
|
|
}
|
|
}
|
|
|
|
history.push(data);
|
|
|
|
if (history.length > 20) {
|
|
history.shift();
|
|
}
|
|
|
|
fs.writeFileSync(RESULTS_FILE, JSON.stringify(history, null, 2));
|
|
}
|
|
|
|
printSummary() {
|
|
console.log('\n📊 性能测试摘要:');
|
|
console.log('═══════════════════════════════════════');
|
|
|
|
const table = this.results.map(r => ({
|
|
端点: r.endpoint,
|
|
方法: r.method,
|
|
平均: `${r.avgDuration.toFixed(0)}ms`,
|
|
最小: `${r.minDuration}ms`,
|
|
最大: `${r.maxDuration}ms`,
|
|
成功率: `${r.successRate}%`,
|
|
状态: r.status
|
|
}));
|
|
|
|
console.table(table);
|
|
|
|
if (this.loadTestResults) {
|
|
console.log('\n📈 负载测试摘要:');
|
|
console.log('═══════════════════════════════════════');
|
|
|
|
const loadTable = this.loadTestResults.map(r => ({
|
|
端点: r.endpoint,
|
|
总请求: r.totalRequests,
|
|
成功: r.successfulRequests,
|
|
失败: r.failedRequests,
|
|
成功率: `${r.successRate}%`,
|
|
平均响应: `${r.avgDuration.toFixed(0)}ms`,
|
|
P95: `${r.p95Duration.toFixed(0)}ms`,
|
|
P99: `${r.p99Duration.toFixed(0)}ms`,
|
|
吞吐量: `${r.throughput} req/s`
|
|
}));
|
|
|
|
console.table(loadTable);
|
|
}
|
|
|
|
console.log('\n💡 性能优化建议:');
|
|
this.printRecommendations();
|
|
}
|
|
|
|
printRecommendations() {
|
|
const slowEndpoints = this.results.filter(r => r.avgDuration > 300);
|
|
if (slowEndpoints.length > 0) {
|
|
console.log(' ⚠️ 以下端点响应时间较长,建议优化:');
|
|
slowEndpoints.forEach(r => {
|
|
console.log(` - ${r.endpoint}: ${r.avgDuration.toFixed(0)}ms`);
|
|
});
|
|
}
|
|
|
|
const lowSuccessRate = this.results.filter(r => parseFloat(r.successRate) < 95);
|
|
if (lowSuccessRate.length > 0) {
|
|
console.log(' ⚠️ 以下端点成功率较低,建议检查:');
|
|
lowSuccessRate.forEach(r => {
|
|
console.log(` - ${r.endpoint}: ${r.successRate}%`);
|
|
});
|
|
}
|
|
|
|
if (slowEndpoints.length === 0 && lowSuccessRate.length === 0) {
|
|
console.log(' ✅ 所有端点性能良好,无需优化');
|
|
}
|
|
}
|
|
|
|
async runLoadTests() {
|
|
console.log('\n📊 开始负载测试...\n');
|
|
|
|
const endpoints = ['/api/users', '/api/roles', '/api/menus'];
|
|
this.loadTestResults = [];
|
|
|
|
for (const endpoint of endpoints) {
|
|
const result = await this.runLoadTest(endpoint, 10, 100);
|
|
this.loadTestResults.push(result);
|
|
|
|
console.log(`\n📈 ${endpoint} 负载测试结果:`);
|
|
console.log(` 成功率: ${result.successRate}%`);
|
|
console.log(` 平均响应时间: ${result.avgDuration.toFixed(0)}ms`);
|
|
console.log(` P95响应时间: ${result.p95Duration.toFixed(0)}ms`);
|
|
console.log(` P99响应时间: ${result.p99Duration.toFixed(0)}ms`);
|
|
console.log(` 吞吐量: ${result.throughput} req/s`);
|
|
}
|
|
|
|
this.saveResults();
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const tester = new PerformanceTester(API_BASE_URL);
|
|
|
|
const command = process.argv[2];
|
|
|
|
switch (command) {
|
|
case 'performance':
|
|
await tester.runPerformanceTests();
|
|
break;
|
|
case 'load':
|
|
await tester.runLoadTests();
|
|
break;
|
|
case 'all':
|
|
await tester.runPerformanceTests();
|
|
await tester.runLoadTests();
|
|
break;
|
|
default:
|
|
console.log('使用方法:');
|
|
console.log(' node scripts/performance-test.js performance - 运行性能测试');
|
|
console.log(' node scripts/performance-test.js load - 运行负载测试');
|
|
console.log(' node scripts/performance-test.js all - 运行所有测试');
|
|
console.log('\n环境变量:');
|
|
console.log(' API_BASE_URL - API基础URL (默认: http://localhost:8080)');
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = PerformanceTester;
|