@testsmith/perfornium 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +14 -0
- package/dist/cli/commands/dashboard.d.ts +9 -0
- package/dist/cli/commands/dashboard.js +98 -0
- package/dist/cli/commands/run.d.ts +4 -0
- package/dist/cli/commands/run.js +143 -14
- package/dist/cli/commands/worker.d.ts +8 -0
- package/dist/cli/commands/worker.js +126 -0
- package/dist/config/types/global-config.d.ts +5 -0
- package/dist/config/types/load-config.d.ts +2 -1
- package/dist/core/test-runner.d.ts +5 -0
- package/dist/core/test-runner.js +124 -5
- package/dist/core/virtual-user.js +13 -3
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/server.d.ts +100 -0
- package/dist/dashboard/server.js +2173 -0
- package/dist/metrics/collector.d.ts +3 -0
- package/dist/metrics/collector.js +69 -13
- package/dist/protocols/rest/handler.d.ts +6 -0
- package/dist/protocols/rest/handler.js +29 -1
- package/dist/protocols/web/core-web-vitals.js +16 -6
- package/dist/protocols/web/handler.js +37 -2
- package/dist/workers/manager.d.ts +1 -0
- package/dist/workers/manager.js +31 -4
- package/dist/workers/worker.js +4 -1
- package/package.json +10 -2
package/dist/core/test-runner.js
CHANGED
|
@@ -52,6 +52,7 @@ const time_1 = require("../utils/time");
|
|
|
52
52
|
const csv_data_provider_1 = require("./csv-data-provider");
|
|
53
53
|
const rendezvous_1 = require("./rendezvous");
|
|
54
54
|
const file_manager_1 = require("../utils/file-manager");
|
|
55
|
+
const dashboard_1 = require("../dashboard");
|
|
55
56
|
class TestRunner {
|
|
56
57
|
constructor(config) {
|
|
57
58
|
this.handlers = new Map();
|
|
@@ -59,6 +60,9 @@ class TestRunner {
|
|
|
59
60
|
this.activeVUs = [];
|
|
60
61
|
this.isRunning = false;
|
|
61
62
|
this.startTime = 0;
|
|
63
|
+
this.testId = '';
|
|
64
|
+
this.dashboardInterval = null;
|
|
65
|
+
this.lastReportedResultIndex = 0;
|
|
62
66
|
this.config = config;
|
|
63
67
|
this.metrics = new collector_1.MetricsCollector();
|
|
64
68
|
// Set log level based on debug config
|
|
@@ -84,8 +88,11 @@ class TestRunner {
|
|
|
84
88
|
logger_1.logger.info(`🚀 Starting test: ${this.config.name}`);
|
|
85
89
|
this.isRunning = true;
|
|
86
90
|
this.startTime = Date.now();
|
|
91
|
+
this.testId = `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
87
92
|
// Reset rendezvous manager for this test run
|
|
88
93
|
rendezvous_1.RendezvousManager.getInstance().reset();
|
|
94
|
+
// Start dashboard reporting if dashboard is running
|
|
95
|
+
this.startDashboardReporting();
|
|
89
96
|
try {
|
|
90
97
|
await this.initialize();
|
|
91
98
|
// NO CSV termination callback setup needed anymore
|
|
@@ -100,6 +107,113 @@ class TestRunner {
|
|
|
100
107
|
}
|
|
101
108
|
finally {
|
|
102
109
|
this.isRunning = false;
|
|
110
|
+
this.stopDashboardReporting();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
startDashboardReporting() {
|
|
114
|
+
const dashboard = (0, dashboard_1.getDashboard)();
|
|
115
|
+
// If running standalone (no dashboard singleton), output progress to stdout for dashboard parsing
|
|
116
|
+
const outputProgress = !dashboard && process.env.PERFORNIUM_PROGRESS !== '0';
|
|
117
|
+
if (dashboard) {
|
|
118
|
+
// Report initial state to in-process dashboard
|
|
119
|
+
dashboard.reportLiveUpdate(this.testId, {
|
|
120
|
+
id: this.testId,
|
|
121
|
+
name: this.config.name,
|
|
122
|
+
startTime: new Date(),
|
|
123
|
+
status: 'running',
|
|
124
|
+
metrics: {
|
|
125
|
+
requests: 0,
|
|
126
|
+
errors: 0,
|
|
127
|
+
avgResponseTime: 0,
|
|
128
|
+
currentVUs: 0
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Track last request count to detect activity
|
|
133
|
+
let lastRequestCount = 0;
|
|
134
|
+
// Report updates every 500ms
|
|
135
|
+
this.dashboardInterval = setInterval(() => {
|
|
136
|
+
if (!this.isRunning)
|
|
137
|
+
return;
|
|
138
|
+
const summary = this.metrics.getSummary();
|
|
139
|
+
const currentVUs = this.activeVUs.filter(vu => vu.isRunning()).length;
|
|
140
|
+
const currentRequests = summary.total_requests || 0;
|
|
141
|
+
// Skip reporting if VUs are 0 and no new requests (test is winding down)
|
|
142
|
+
const hasActivity = currentVUs > 0 || currentRequests > lastRequestCount;
|
|
143
|
+
lastRequestCount = currentRequests;
|
|
144
|
+
if (dashboard) {
|
|
145
|
+
dashboard.reportLiveUpdate(this.testId, {
|
|
146
|
+
metrics: {
|
|
147
|
+
requests: currentRequests,
|
|
148
|
+
errors: summary.failed_requests || 0,
|
|
149
|
+
avgResponseTime: summary.avg_response_time || 0,
|
|
150
|
+
currentVUs
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Output machine-readable progress for dashboard parsing (only when there's activity)
|
|
155
|
+
if (outputProgress && hasActivity) {
|
|
156
|
+
const rps = summary.requests_per_second || 0;
|
|
157
|
+
const p50 = summary.percentiles?.[50] || 0;
|
|
158
|
+
const p90 = summary.percentiles?.[90] || 0;
|
|
159
|
+
const p95 = summary.percentiles?.[95] || 0;
|
|
160
|
+
const p99 = summary.percentiles?.[99] || 0;
|
|
161
|
+
const successRate = summary.success_rate || 0;
|
|
162
|
+
// Main progress line with percentiles
|
|
163
|
+
console.log(`[PROGRESS] VUs: ${currentVUs} | Requests: ${currentRequests} | Errors: ${summary.failed_requests || 0} | Avg RT: ${(summary.avg_response_time || 0).toFixed(0)}ms | RPS: ${rps.toFixed(1)} | P50: ${p50.toFixed(0)}ms | P90: ${p90.toFixed(0)}ms | P95: ${p95.toFixed(0)}ms | P99: ${p99.toFixed(0)}ms | Success: ${successRate.toFixed(1)}%`);
|
|
164
|
+
// Output step statistics if available
|
|
165
|
+
if (summary.step_statistics && summary.step_statistics.length > 0) {
|
|
166
|
+
const stepData = summary.step_statistics.map(s => ({
|
|
167
|
+
n: s.step_name,
|
|
168
|
+
s: s.scenario,
|
|
169
|
+
r: s.total_requests,
|
|
170
|
+
e: s.failed_requests,
|
|
171
|
+
a: Math.round(s.avg_response_time),
|
|
172
|
+
p50: Math.round(s.percentiles?.[50] || 0),
|
|
173
|
+
p95: Math.round(s.percentiles?.[95] || 0),
|
|
174
|
+
p99: Math.round(s.percentiles?.[99] || 0),
|
|
175
|
+
sr: Math.round(s.success_rate * 10) / 10
|
|
176
|
+
}));
|
|
177
|
+
console.log(`[STEPS] ${JSON.stringify(stepData)}`);
|
|
178
|
+
}
|
|
179
|
+
// Output individual response times (last 50 new results)
|
|
180
|
+
const allResults = this.metrics.getResults();
|
|
181
|
+
if (allResults.length > this.lastReportedResultIndex) {
|
|
182
|
+
const newResults = allResults.slice(this.lastReportedResultIndex, this.lastReportedResultIndex + 50);
|
|
183
|
+
const rtData = newResults.map(r => ({
|
|
184
|
+
t: r.timestamp,
|
|
185
|
+
v: Math.round(r.duration),
|
|
186
|
+
s: r.success ? 1 : 0,
|
|
187
|
+
n: r.step_name || r.action || 'unknown' // Include step/request name for coloring
|
|
188
|
+
}));
|
|
189
|
+
if (rtData.length > 0) {
|
|
190
|
+
console.log(`[RT] ${JSON.stringify(rtData)}`);
|
|
191
|
+
}
|
|
192
|
+
this.lastReportedResultIndex = Math.min(allResults.length, this.lastReportedResultIndex + 50);
|
|
193
|
+
}
|
|
194
|
+
// Output top 10 errors if any
|
|
195
|
+
if (summary.error_details && summary.error_details.length > 0) {
|
|
196
|
+
const topErrors = summary.error_details.slice(0, 10).map((e) => ({
|
|
197
|
+
scenario: e.scenario,
|
|
198
|
+
action: e.action,
|
|
199
|
+
status: e.status,
|
|
200
|
+
error: e.error?.substring(0, 200),
|
|
201
|
+
url: e.request_url,
|
|
202
|
+
count: e.count
|
|
203
|
+
}));
|
|
204
|
+
console.log(`[ERRORS] ${JSON.stringify(topErrors)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}, 500);
|
|
208
|
+
}
|
|
209
|
+
stopDashboardReporting() {
|
|
210
|
+
if (this.dashboardInterval) {
|
|
211
|
+
clearInterval(this.dashboardInterval);
|
|
212
|
+
this.dashboardInterval = null;
|
|
213
|
+
}
|
|
214
|
+
const dashboard = (0, dashboard_1.getDashboard)();
|
|
215
|
+
if (dashboard) {
|
|
216
|
+
dashboard.reportTestComplete(this.testId);
|
|
103
217
|
}
|
|
104
218
|
}
|
|
105
219
|
setupCSVBaseDirectory() {
|
|
@@ -356,19 +470,24 @@ class TestRunner {
|
|
|
356
470
|
}
|
|
357
471
|
async waitForVUsToComplete(timeoutMs = 60000) {
|
|
358
472
|
const startTime = Date.now();
|
|
359
|
-
|
|
473
|
+
// Filter to only running VUs
|
|
474
|
+
const getRunningVUs = () => this.activeVUs.filter(vu => vu.isRunning());
|
|
475
|
+
while (getRunningVUs().length > 0 && this.isRunning) {
|
|
360
476
|
const elapsed = Date.now() - startTime;
|
|
477
|
+
const runningCount = getRunningVUs().length;
|
|
361
478
|
if (elapsed > timeoutMs) {
|
|
362
|
-
logger_1.logger.warn(`⚠️ Timeout waiting for ${
|
|
479
|
+
logger_1.logger.warn(`⚠️ Timeout waiting for ${runningCount} VUs to complete`);
|
|
363
480
|
// Force stop remaining VUs
|
|
364
481
|
this.activeVUs.forEach(vu => vu.stop());
|
|
365
482
|
break;
|
|
366
483
|
}
|
|
367
|
-
await (0, time_1.sleep)(
|
|
368
|
-
if (
|
|
369
|
-
logger_1.logger.debug(`⏳ Waiting for ${
|
|
484
|
+
await (0, time_1.sleep)(100); // Check more frequently
|
|
485
|
+
if (runningCount > 0 && elapsed % 5000 === 0) { // Log every 5 seconds
|
|
486
|
+
logger_1.logger.debug(`⏳ Waiting for ${runningCount} VUs to complete...`);
|
|
370
487
|
}
|
|
371
488
|
}
|
|
489
|
+
// Clear the activeVUs array
|
|
490
|
+
this.activeVUs.length = 0;
|
|
372
491
|
logger_1.logger.debug('✅ All VUs completed');
|
|
373
492
|
}
|
|
374
493
|
async cleanup() {
|
|
@@ -258,9 +258,19 @@ class VirtualUser {
|
|
|
258
258
|
this.metrics.recordResult(result);
|
|
259
259
|
}
|
|
260
260
|
// Apply hierarchical think time: step > scenario > global
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
// Skip think time if the NEXT step is a verification/wait step - they measure app
|
|
262
|
+
// responsiveness and should run immediately after the triggering action
|
|
263
|
+
const nextStep = scenario.steps[stepIndex + 1];
|
|
264
|
+
const nextCommand = nextStep?.action?.command || '';
|
|
265
|
+
const nextIsVerificationOrWait = nextCommand.startsWith('verify_') ||
|
|
266
|
+
nextCommand.startsWith('wait_for_') ||
|
|
267
|
+
nextCommand === 'measure_web_vitals' ||
|
|
268
|
+
nextCommand === 'performance_audit';
|
|
269
|
+
if (!nextIsVerificationOrWait) {
|
|
270
|
+
const effectiveThinkTime = this.getEffectiveThinkTime(step, scenario);
|
|
271
|
+
if (effectiveThinkTime !== undefined) {
|
|
272
|
+
await this.applyThinkTime(effectiveThinkTime);
|
|
273
|
+
}
|
|
264
274
|
}
|
|
265
275
|
}
|
|
266
276
|
catch (error) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DashboardServer, getDashboard, setDashboard } from './server';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setDashboard = exports.getDashboard = exports.DashboardServer = void 0;
|
|
4
|
+
var server_1 = require("./server");
|
|
5
|
+
Object.defineProperty(exports, "DashboardServer", { enumerable: true, get: function () { return server_1.DashboardServer; } });
|
|
6
|
+
Object.defineProperty(exports, "getDashboard", { enumerable: true, get: function () { return server_1.getDashboard; } });
|
|
7
|
+
Object.defineProperty(exports, "setDashboard", { enumerable: true, get: function () { return server_1.setDashboard; } });
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
interface DashboardOptions {
|
|
2
|
+
port: number;
|
|
3
|
+
resultsDir: string;
|
|
4
|
+
testsDir?: string;
|
|
5
|
+
workersFile?: string;
|
|
6
|
+
}
|
|
7
|
+
interface LiveTest {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
startTime: Date;
|
|
11
|
+
status: 'running' | 'completed' | 'failed';
|
|
12
|
+
metrics: {
|
|
13
|
+
requests: number;
|
|
14
|
+
errors: number;
|
|
15
|
+
avgResponseTime: number;
|
|
16
|
+
currentVUs: number;
|
|
17
|
+
p50ResponseTime?: number;
|
|
18
|
+
p90ResponseTime?: number;
|
|
19
|
+
p95ResponseTime?: number;
|
|
20
|
+
p99ResponseTime?: number;
|
|
21
|
+
minResponseTime?: number;
|
|
22
|
+
maxResponseTime?: number;
|
|
23
|
+
requestsPerSecond?: number;
|
|
24
|
+
successRate?: number;
|
|
25
|
+
};
|
|
26
|
+
stepStats: {
|
|
27
|
+
stepName: string;
|
|
28
|
+
scenario: string;
|
|
29
|
+
requests: number;
|
|
30
|
+
errors: number;
|
|
31
|
+
avgResponseTime: number;
|
|
32
|
+
p50?: number;
|
|
33
|
+
p95?: number;
|
|
34
|
+
p99?: number;
|
|
35
|
+
successRate: number;
|
|
36
|
+
}[];
|
|
37
|
+
responseTimes: {
|
|
38
|
+
timestamp: number;
|
|
39
|
+
value: number;
|
|
40
|
+
success: boolean;
|
|
41
|
+
stepName?: string;
|
|
42
|
+
}[];
|
|
43
|
+
topErrors: {
|
|
44
|
+
scenario: string;
|
|
45
|
+
action: string;
|
|
46
|
+
status?: number;
|
|
47
|
+
error: string;
|
|
48
|
+
url?: string;
|
|
49
|
+
count: number;
|
|
50
|
+
}[];
|
|
51
|
+
history: {
|
|
52
|
+
timestamp: number;
|
|
53
|
+
requests: number;
|
|
54
|
+
errors: number;
|
|
55
|
+
avgResponseTime: number;
|
|
56
|
+
p95ResponseTime: number;
|
|
57
|
+
p99ResponseTime: number;
|
|
58
|
+
vus: number;
|
|
59
|
+
rps: number;
|
|
60
|
+
}[];
|
|
61
|
+
}
|
|
62
|
+
export declare class DashboardServer {
|
|
63
|
+
private server;
|
|
64
|
+
private wss;
|
|
65
|
+
private options;
|
|
66
|
+
private clients;
|
|
67
|
+
private liveTests;
|
|
68
|
+
private runningProcesses;
|
|
69
|
+
constructor(options: DashboardOptions);
|
|
70
|
+
start(): Promise<void>;
|
|
71
|
+
stop(): Promise<void>;
|
|
72
|
+
reportLiveUpdate(testId: string, update: Partial<LiveTest>): void;
|
|
73
|
+
reportTestComplete(testId: string): void;
|
|
74
|
+
private broadcast;
|
|
75
|
+
private handleRequest;
|
|
76
|
+
private handleGetResults;
|
|
77
|
+
private handleGetResult;
|
|
78
|
+
private handleDeleteResult;
|
|
79
|
+
private handleCompare;
|
|
80
|
+
private handleGetLive;
|
|
81
|
+
private handleGetTests;
|
|
82
|
+
private handleGetWorkers;
|
|
83
|
+
private handleRunTest;
|
|
84
|
+
private parseOutputForMetrics;
|
|
85
|
+
private handleStopTest;
|
|
86
|
+
private readBody;
|
|
87
|
+
private scanTestFiles;
|
|
88
|
+
private scanDirForTests;
|
|
89
|
+
private scanResults;
|
|
90
|
+
private extractSummary;
|
|
91
|
+
private loadFullResult;
|
|
92
|
+
private generateComparison;
|
|
93
|
+
private generateStepComparisons;
|
|
94
|
+
private calcDiff;
|
|
95
|
+
private serveStatic;
|
|
96
|
+
private getDashboardHTML;
|
|
97
|
+
}
|
|
98
|
+
export declare function getDashboard(): DashboardServer | null;
|
|
99
|
+
export declare function setDashboard(dashboard: DashboardServer): void;
|
|
100
|
+
export {};
|