@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.
@@ -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
- while (this.activeVUs.length > 0 && this.isRunning) {
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 ${this.activeVUs.length} VUs to complete`);
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)(1000);
368
- if (this.activeVUs.length > 0 && elapsed % 5000 === 0) { // Log every 5 seconds
369
- logger_1.logger.debug(`⏳ Waiting for ${this.activeVUs.length} VUs to complete...`);
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
- const effectiveThinkTime = this.getEffectiveThinkTime(step, scenario);
262
- if (effectiveThinkTime !== undefined) {
263
- await this.applyThinkTime(effectiveThinkTime);
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 {};