@testsmith/perfornium 0.1.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/README.md +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- package/package.json +102 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RemoteWorker } from './remote-worker';
|
|
2
|
+
export interface WorkerHealth {
|
|
3
|
+
address: string;
|
|
4
|
+
lastHeartbeat: number;
|
|
5
|
+
isHealthy: boolean;
|
|
6
|
+
responseTime: number;
|
|
7
|
+
errorCount: number;
|
|
8
|
+
status: 'connected' | 'disconnected' | 'unhealthy' | 'timeout';
|
|
9
|
+
}
|
|
10
|
+
export declare class HealthMonitor {
|
|
11
|
+
private workerHealth;
|
|
12
|
+
private monitorInterval?;
|
|
13
|
+
private isMonitoring;
|
|
14
|
+
start(workers: RemoteWorker[], intervalMs?: number): void;
|
|
15
|
+
stop(): void;
|
|
16
|
+
updateWorkerStatus(address: string, status: any): void;
|
|
17
|
+
private checkWorkerHealth;
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HealthMonitor = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
class HealthMonitor {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.workerHealth = new Map();
|
|
8
|
+
this.isMonitoring = false;
|
|
9
|
+
}
|
|
10
|
+
start(workers, intervalMs = 30000) {
|
|
11
|
+
this.isMonitoring = true;
|
|
12
|
+
// Initialize health tracking
|
|
13
|
+
workers.forEach(worker => {
|
|
14
|
+
this.workerHealth.set(worker.getAddress(), {
|
|
15
|
+
address: worker.getAddress(),
|
|
16
|
+
lastHeartbeat: Date.now(),
|
|
17
|
+
isHealthy: true,
|
|
18
|
+
responseTime: 0,
|
|
19
|
+
errorCount: 0,
|
|
20
|
+
status: 'connected'
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
// Start monitoring loop
|
|
24
|
+
this.monitorInterval = setInterval(() => {
|
|
25
|
+
this.checkWorkerHealth();
|
|
26
|
+
}, intervalMs);
|
|
27
|
+
logger_1.logger.debug(`💓 Health monitoring started (${intervalMs}ms interval)`);
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
this.isMonitoring = false;
|
|
31
|
+
if (this.monitorInterval) {
|
|
32
|
+
clearInterval(this.monitorInterval);
|
|
33
|
+
this.monitorInterval = undefined;
|
|
34
|
+
}
|
|
35
|
+
logger_1.logger.debug('💓 Health monitoring stopped');
|
|
36
|
+
}
|
|
37
|
+
updateWorkerStatus(address, status) {
|
|
38
|
+
const health = this.workerHealth.get(address);
|
|
39
|
+
if (health) {
|
|
40
|
+
health.lastHeartbeat = Date.now();
|
|
41
|
+
health.responseTime = status.responseTime || 0;
|
|
42
|
+
health.status = 'connected';
|
|
43
|
+
if (status.error) {
|
|
44
|
+
health.errorCount++;
|
|
45
|
+
}
|
|
46
|
+
this.workerHealth.set(address, health);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
checkWorkerHealth() {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const timeoutMs = 60000; // 1 minute timeout
|
|
52
|
+
for (const [address, health] of this.workerHealth) {
|
|
53
|
+
const timeSinceHeartbeat = now - health.lastHeartbeat;
|
|
54
|
+
if (timeSinceHeartbeat > timeoutMs) {
|
|
55
|
+
health.isHealthy = false;
|
|
56
|
+
health.status = 'timeout';
|
|
57
|
+
logger_1.logger.warn(`⚠️ Worker ${address} health check timeout`);
|
|
58
|
+
}
|
|
59
|
+
else if (health.errorCount > 10) {
|
|
60
|
+
health.isHealthy = false;
|
|
61
|
+
health.status = 'unhealthy';
|
|
62
|
+
logger_1.logger.warn(`⚠️ Worker ${address} has too many errors`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
health.isHealthy = true;
|
|
66
|
+
health.status = 'connected';
|
|
67
|
+
}
|
|
68
|
+
this.workerHealth.set(address, health);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.HealthMonitor = HealthMonitor;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TestConfiguration } from '../config';
|
|
2
|
+
import { RemoteWorker } from './remote-worker';
|
|
3
|
+
export type DistributionStrategy = 'even' | 'capacity_based' | 'round_robin' | 'geographic';
|
|
4
|
+
export interface WorkerAssignment {
|
|
5
|
+
worker: RemoteWorker;
|
|
6
|
+
config: TestConfiguration;
|
|
7
|
+
virtualUsers: number;
|
|
8
|
+
startDelay?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class LoadDistributor {
|
|
11
|
+
distribute(testConfig: TestConfiguration, workers: RemoteWorker[], strategy: DistributionStrategy): WorkerAssignment[];
|
|
12
|
+
private evenDistribution;
|
|
13
|
+
private capacityBasedDistribution;
|
|
14
|
+
private roundRobinDistribution;
|
|
15
|
+
private geographicDistribution;
|
|
16
|
+
private createWorkerConfig;
|
|
17
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoadDistributor = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
class LoadDistributor {
|
|
6
|
+
distribute(testConfig, workers, strategy) {
|
|
7
|
+
const { getPrimaryLoadPhase } = require('../config/types/load-config');
|
|
8
|
+
const primaryPhase = getPrimaryLoadPhase(testConfig.load);
|
|
9
|
+
const totalVUs = primaryPhase.virtual_users || primaryPhase.vus || 1;
|
|
10
|
+
logger_1.logger.info(`📊 Distributing ${totalVUs} VUs using ${strategy} strategy`);
|
|
11
|
+
switch (strategy) {
|
|
12
|
+
case 'even':
|
|
13
|
+
return this.evenDistribution(testConfig, workers, totalVUs);
|
|
14
|
+
case 'capacity_based':
|
|
15
|
+
return this.capacityBasedDistribution(testConfig, workers, totalVUs);
|
|
16
|
+
case 'round_robin':
|
|
17
|
+
return this.roundRobinDistribution(testConfig, workers, totalVUs);
|
|
18
|
+
case 'geographic':
|
|
19
|
+
return this.geographicDistribution(testConfig, workers, totalVUs);
|
|
20
|
+
default:
|
|
21
|
+
throw new Error(`Unknown distribution strategy: ${strategy}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
evenDistribution(testConfig, workers, totalVUs) {
|
|
25
|
+
const vusPerWorker = Math.floor(totalVUs / workers.length);
|
|
26
|
+
const remainder = totalVUs % workers.length;
|
|
27
|
+
return workers.map((worker, index) => {
|
|
28
|
+
const virtualUsers = vusPerWorker + (index < remainder ? 1 : 0);
|
|
29
|
+
return {
|
|
30
|
+
worker,
|
|
31
|
+
virtualUsers,
|
|
32
|
+
config: this.createWorkerConfig(testConfig, virtualUsers, index)
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
capacityBasedDistribution(testConfig, workers, totalVUs) {
|
|
37
|
+
const totalCapacity = workers.reduce((sum, worker) => sum + worker.getCapacity(), 0);
|
|
38
|
+
if (totalCapacity === 0) {
|
|
39
|
+
throw new Error('No worker capacity available');
|
|
40
|
+
}
|
|
41
|
+
return workers.map((worker, index) => {
|
|
42
|
+
const workerRatio = worker.getCapacity() / totalCapacity;
|
|
43
|
+
const virtualUsers = Math.round(totalVUs * workerRatio);
|
|
44
|
+
return {
|
|
45
|
+
worker,
|
|
46
|
+
virtualUsers,
|
|
47
|
+
config: this.createWorkerConfig(testConfig, virtualUsers, index)
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
roundRobinDistribution(testConfig, workers, totalVUs) {
|
|
52
|
+
const assignments = workers.map((worker, index) => ({
|
|
53
|
+
worker,
|
|
54
|
+
virtualUsers: 0,
|
|
55
|
+
config: this.createWorkerConfig(testConfig, 0, index)
|
|
56
|
+
}));
|
|
57
|
+
for (let vu = 0; vu < totalVUs; vu++) {
|
|
58
|
+
const workerIndex = vu % workers.length;
|
|
59
|
+
assignments[workerIndex].virtualUsers++;
|
|
60
|
+
}
|
|
61
|
+
assignments.forEach((assignment, index) => {
|
|
62
|
+
assignment.config = this.createWorkerConfig(testConfig, assignment.virtualUsers, index);
|
|
63
|
+
});
|
|
64
|
+
return assignments;
|
|
65
|
+
}
|
|
66
|
+
geographicDistribution(testConfig, workers, totalVUs) {
|
|
67
|
+
const regionGroups = new Map();
|
|
68
|
+
workers.forEach(worker => {
|
|
69
|
+
const region = worker.getRegion();
|
|
70
|
+
if (!regionGroups.has(region)) {
|
|
71
|
+
regionGroups.set(region, []);
|
|
72
|
+
}
|
|
73
|
+
regionGroups.get(region).push(worker);
|
|
74
|
+
});
|
|
75
|
+
const vusPerRegion = Math.floor(totalVUs / regionGroups.size);
|
|
76
|
+
const regionRemainder = totalVUs % regionGroups.size;
|
|
77
|
+
const assignments = [];
|
|
78
|
+
let regionIndex = 0;
|
|
79
|
+
for (const [region, regionWorkers] of regionGroups) {
|
|
80
|
+
const regionVUs = vusPerRegion + (regionIndex < regionRemainder ? 1 : 0);
|
|
81
|
+
const regionAssignments = this.evenDistribution(testConfig, regionWorkers, regionVUs);
|
|
82
|
+
assignments.push(...regionAssignments);
|
|
83
|
+
regionIndex++;
|
|
84
|
+
}
|
|
85
|
+
return assignments;
|
|
86
|
+
}
|
|
87
|
+
createWorkerConfig(baseConfig, virtualUsers, workerIndex) {
|
|
88
|
+
// Fixed: Ensure report config has required fields
|
|
89
|
+
const reportConfig = baseConfig.report ? {
|
|
90
|
+
...baseConfig.report,
|
|
91
|
+
generate: false, // Workers shouldn't generate reports
|
|
92
|
+
output: baseConfig.report.output || 'worker-report.html' // Provide default
|
|
93
|
+
} : undefined;
|
|
94
|
+
return {
|
|
95
|
+
...baseConfig,
|
|
96
|
+
name: `${baseConfig.name} - Worker ${workerIndex + 1}`,
|
|
97
|
+
load: {
|
|
98
|
+
...baseConfig.load,
|
|
99
|
+
virtual_users: virtualUsers
|
|
100
|
+
},
|
|
101
|
+
outputs: baseConfig.outputs?.filter(output => output.type !== 'webhook'),
|
|
102
|
+
report: reportConfig
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.LoadDistributor = LoadDistributor;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { TestConfiguration } from '../config/types';
|
|
3
|
+
export interface RemoteWorkerConfig {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
capacity: number;
|
|
7
|
+
region: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkerStatus {
|
|
10
|
+
connected: boolean;
|
|
11
|
+
running: boolean;
|
|
12
|
+
virtualUsers: number;
|
|
13
|
+
requestsPerSecond: number;
|
|
14
|
+
responseTime: number;
|
|
15
|
+
errorRate: number;
|
|
16
|
+
activeRunner?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class RemoteWorker extends EventEmitter {
|
|
19
|
+
private config;
|
|
20
|
+
private connected;
|
|
21
|
+
private status;
|
|
22
|
+
constructor(config: RemoteWorkerConfig);
|
|
23
|
+
connect(): Promise<void>;
|
|
24
|
+
disconnect(): Promise<void>;
|
|
25
|
+
prepareTest(testConfig: TestConfiguration): Promise<void>;
|
|
26
|
+
startTest(startTime?: number): Promise<void>;
|
|
27
|
+
executeTest(testConfig: TestConfiguration): Promise<void>;
|
|
28
|
+
stop(): Promise<void>;
|
|
29
|
+
waitForCompletion(): Promise<void>;
|
|
30
|
+
getWorkerStatus(): Promise<WorkerStatus>;
|
|
31
|
+
private sendHealthCheck;
|
|
32
|
+
private sendRequest;
|
|
33
|
+
getAddress(): string;
|
|
34
|
+
getCapacity(): number;
|
|
35
|
+
getRegion(): string;
|
|
36
|
+
getResults(): Promise<any>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RemoteWorker = void 0;
|
|
37
|
+
const events_1 = require("events");
|
|
38
|
+
const logger_1 = require("../utils/logger");
|
|
39
|
+
const http = __importStar(require("http"));
|
|
40
|
+
class RemoteWorker extends events_1.EventEmitter {
|
|
41
|
+
constructor(config) {
|
|
42
|
+
super();
|
|
43
|
+
this.connected = false;
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.status = {
|
|
46
|
+
connected: false,
|
|
47
|
+
running: false,
|
|
48
|
+
virtualUsers: 0,
|
|
49
|
+
requestsPerSecond: 0,
|
|
50
|
+
responseTime: 0,
|
|
51
|
+
errorRate: 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async connect() {
|
|
55
|
+
try {
|
|
56
|
+
// Implement connection logic to worker
|
|
57
|
+
await this.sendHealthCheck();
|
|
58
|
+
this.connected = true;
|
|
59
|
+
this.status.connected = true;
|
|
60
|
+
logger_1.logger.debug(`🔗 Connected to worker ${this.getAddress()}`);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
this.connected = false;
|
|
64
|
+
this.status.connected = false;
|
|
65
|
+
throw new Error(`Failed to connect to worker ${this.getAddress()}: ${error}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async disconnect() {
|
|
69
|
+
this.connected = false;
|
|
70
|
+
this.status.connected = false;
|
|
71
|
+
logger_1.logger.debug(`🔌 Disconnected from worker ${this.getAddress()}`);
|
|
72
|
+
}
|
|
73
|
+
async prepareTest(testConfig) {
|
|
74
|
+
if (!this.connected) {
|
|
75
|
+
throw new Error(`Worker ${this.getAddress()} not connected`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await this.sendRequest('/prepare', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
data: testConfig
|
|
81
|
+
});
|
|
82
|
+
logger_1.logger.debug(`📋 Test prepared on worker ${this.getAddress()}`);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new Error(`Failed to prepare test on worker ${this.getAddress()}: ${error}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async startTest(startTime) {
|
|
89
|
+
if (!this.connected) {
|
|
90
|
+
throw new Error(`Worker ${this.getAddress()} not connected`);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await this.sendRequest('/start', {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
data: { startTime }
|
|
96
|
+
});
|
|
97
|
+
this.status.running = true;
|
|
98
|
+
logger_1.logger.debug(`🚀 Test started on worker ${this.getAddress()}`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new Error(`Failed to start test on worker ${this.getAddress()}: ${error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async executeTest(testConfig) {
|
|
105
|
+
await this.prepareTest(testConfig);
|
|
106
|
+
await this.startTest();
|
|
107
|
+
}
|
|
108
|
+
async stop() {
|
|
109
|
+
if (!this.connected) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
await this.sendRequest('/stop', { method: 'POST' });
|
|
114
|
+
this.status.running = false;
|
|
115
|
+
logger_1.logger.debug(`⏹️ Test stopped on worker ${this.getAddress()}`);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger_1.logger.warn(`Failed to stop test on worker ${this.getAddress()}: ${error}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async waitForCompletion() {
|
|
122
|
+
if (!this.status.running) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const checkCompletion = async () => {
|
|
127
|
+
try {
|
|
128
|
+
const status = await this.getWorkerStatus();
|
|
129
|
+
logger_1.logger.debug(`🔍 Worker ${this.getAddress()} status: running=${status.running}, activeRunner=${status.activeRunner}`);
|
|
130
|
+
// Check if worker is no longer running AND has no active runner
|
|
131
|
+
if (!status.running && (!status.activeRunner || status.activeRunner === 'idle')) {
|
|
132
|
+
this.status.running = false;
|
|
133
|
+
logger_1.logger.debug(`✅ Worker ${this.getAddress()} completed`);
|
|
134
|
+
resolve();
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Continue checking
|
|
138
|
+
setTimeout(checkCompletion, 2000); // Check every 2 seconds
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
logger_1.logger.warn(`⚠️ Error checking worker ${this.getAddress()} status:`, error);
|
|
143
|
+
// On connection error, assume worker completed
|
|
144
|
+
this.status.running = false;
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
checkCompletion();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async getWorkerStatus() {
|
|
152
|
+
if (!this.connected) {
|
|
153
|
+
return this.status;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const response = await this.sendRequest('/status', { method: 'GET' });
|
|
157
|
+
this.status = { ...this.status, ...response };
|
|
158
|
+
this.emit('status', this.status);
|
|
159
|
+
return this.status;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
this.emit('error', error);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async sendHealthCheck() {
|
|
167
|
+
const response = await this.sendRequest('/health', { method: 'GET' });
|
|
168
|
+
if (response.status !== 'healthy') {
|
|
169
|
+
throw new Error(`Worker health check failed: ${JSON.stringify(response)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async sendRequest(path, options) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const postData = options.data ? JSON.stringify(options.data) : undefined;
|
|
175
|
+
const requestOptions = {
|
|
176
|
+
hostname: this.config.host,
|
|
177
|
+
port: this.config.port,
|
|
178
|
+
path,
|
|
179
|
+
method: options.method,
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
...(postData && { 'Content-Length': Buffer.byteLength(postData) })
|
|
183
|
+
},
|
|
184
|
+
timeout: 30000
|
|
185
|
+
};
|
|
186
|
+
const req = http.request(requestOptions, (res) => {
|
|
187
|
+
let data = '';
|
|
188
|
+
res.on('data', (chunk) => {
|
|
189
|
+
data += chunk;
|
|
190
|
+
});
|
|
191
|
+
res.on('end', () => {
|
|
192
|
+
try {
|
|
193
|
+
const response = data ? JSON.parse(data) : {};
|
|
194
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
195
|
+
resolve(response);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
reject(new Error(`HTTP ${res.statusCode}: ${response.message || 'Request failed'}`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
reject(new Error(`Invalid JSON response: ${error}`));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
req.on('error', (error) => {
|
|
207
|
+
reject(error);
|
|
208
|
+
});
|
|
209
|
+
req.on('timeout', () => {
|
|
210
|
+
req.destroy();
|
|
211
|
+
reject(new Error('Request timeout'));
|
|
212
|
+
});
|
|
213
|
+
if (postData) {
|
|
214
|
+
req.write(postData);
|
|
215
|
+
}
|
|
216
|
+
req.end();
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
getAddress() {
|
|
220
|
+
return `${this.config.host}:${this.config.port}`;
|
|
221
|
+
}
|
|
222
|
+
getCapacity() {
|
|
223
|
+
return this.config.capacity;
|
|
224
|
+
}
|
|
225
|
+
getRegion() {
|
|
226
|
+
return this.config.region;
|
|
227
|
+
}
|
|
228
|
+
async getResults() {
|
|
229
|
+
if (!this.connected) {
|
|
230
|
+
throw new Error(`Worker ${this.getAddress()} not connected`);
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
return await this.sendRequest('/results', { method: 'GET' });
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
this.emit('error', error);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
exports.RemoteWorker = RemoteWorker;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TestResult, VUStartEvent } from '../metrics/types';
|
|
2
|
+
export interface AggregatedResults {
|
|
3
|
+
summary: {
|
|
4
|
+
total_requests: number;
|
|
5
|
+
success_rate: number;
|
|
6
|
+
avg_response_time: number;
|
|
7
|
+
requests_per_second: number;
|
|
8
|
+
total_errors: number;
|
|
9
|
+
start_time: number;
|
|
10
|
+
end_time: number;
|
|
11
|
+
duration: number;
|
|
12
|
+
total_duration: number;
|
|
13
|
+
total_virtual_users: number;
|
|
14
|
+
peak_virtual_users: number;
|
|
15
|
+
successful_requests: number;
|
|
16
|
+
failed_requests: number;
|
|
17
|
+
vu_ramp_up: VUStartEvent[];
|
|
18
|
+
};
|
|
19
|
+
results: TestResult[];
|
|
20
|
+
workers: {
|
|
21
|
+
[workerAddress: string]: {
|
|
22
|
+
requests: number;
|
|
23
|
+
errors: number;
|
|
24
|
+
avg_response_time: number;
|
|
25
|
+
requests_per_second: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare class ResultAggregator {
|
|
30
|
+
private results;
|
|
31
|
+
private workerResults;
|
|
32
|
+
private vuRampUpEvents;
|
|
33
|
+
private workerVUEvents;
|
|
34
|
+
private isAggregating;
|
|
35
|
+
private startTime;
|
|
36
|
+
private endTime;
|
|
37
|
+
start(): void;
|
|
38
|
+
stop(): void;
|
|
39
|
+
addResult(result: TestResult, workerAddress?: string): void;
|
|
40
|
+
addVURampUpEvents(events: VUStartEvent[], workerAddress: string): void;
|
|
41
|
+
getAggregatedResults(): AggregatedResults;
|
|
42
|
+
clear(): void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResultAggregator = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
class ResultAggregator {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.results = [];
|
|
8
|
+
this.workerResults = new Map();
|
|
9
|
+
this.vuRampUpEvents = [];
|
|
10
|
+
this.workerVUEvents = new Map();
|
|
11
|
+
this.isAggregating = false;
|
|
12
|
+
this.startTime = 0;
|
|
13
|
+
this.endTime = 0;
|
|
14
|
+
}
|
|
15
|
+
start() {
|
|
16
|
+
this.isAggregating = true;
|
|
17
|
+
this.startTime = Date.now();
|
|
18
|
+
this.results = [];
|
|
19
|
+
this.workerResults.clear();
|
|
20
|
+
this.vuRampUpEvents = [];
|
|
21
|
+
this.workerVUEvents.clear();
|
|
22
|
+
logger_1.logger.debug('📊 Result aggregation started');
|
|
23
|
+
}
|
|
24
|
+
stop() {
|
|
25
|
+
this.isAggregating = false;
|
|
26
|
+
this.endTime = Date.now();
|
|
27
|
+
logger_1.logger.debug('📊 Result aggregation stopped');
|
|
28
|
+
}
|
|
29
|
+
addResult(result, workerAddress) {
|
|
30
|
+
if (!this.isAggregating) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.results.push(result);
|
|
34
|
+
if (workerAddress) {
|
|
35
|
+
if (!this.workerResults.has(workerAddress)) {
|
|
36
|
+
this.workerResults.set(workerAddress, []);
|
|
37
|
+
}
|
|
38
|
+
this.workerResults.get(workerAddress).push(result);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
addVURampUpEvents(events, workerAddress) {
|
|
42
|
+
if (!this.isAggregating || !events || events.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Store per-worker VU events (with worker-unique VU IDs)
|
|
46
|
+
if (!this.workerVUEvents.has(workerAddress)) {
|
|
47
|
+
this.workerVUEvents.set(workerAddress, []);
|
|
48
|
+
}
|
|
49
|
+
// Create globally unique VU IDs by prefixing with worker index
|
|
50
|
+
const workerIndex = Array.from(this.workerVUEvents.keys()).indexOf(workerAddress);
|
|
51
|
+
const workerPrefix = workerIndex >= 0 ? workerIndex : this.workerVUEvents.size;
|
|
52
|
+
const maxVUsPerWorker = 10000; // Allows up to 10000 VUs per worker
|
|
53
|
+
events.forEach(event => {
|
|
54
|
+
// Create a globally unique VU ID: workerPrefix * maxVUsPerWorker + original vu_id
|
|
55
|
+
const globalVUId = (workerPrefix * maxVUsPerWorker) + event.vu_id;
|
|
56
|
+
const globalEvent = {
|
|
57
|
+
...event,
|
|
58
|
+
vu_id: globalVUId
|
|
59
|
+
};
|
|
60
|
+
this.vuRampUpEvents.push(globalEvent);
|
|
61
|
+
this.workerVUEvents.get(workerAddress).push(event); // Keep original for per-worker tracking
|
|
62
|
+
});
|
|
63
|
+
logger_1.logger.debug(`📊 Added ${events.length} VU ramp-up events from ${workerAddress}`);
|
|
64
|
+
}
|
|
65
|
+
getAggregatedResults() {
|
|
66
|
+
const totalRequests = this.results.length;
|
|
67
|
+
const successfulRequests = this.results.filter(r => r.success).length;
|
|
68
|
+
const failedRequests = totalRequests - successfulRequests;
|
|
69
|
+
const successRate = totalRequests > 0 ? (successfulRequests / totalRequests) * 100 : 0;
|
|
70
|
+
const responseTimes = this.results.map(r => r.duration || r.response_size || 0);
|
|
71
|
+
const avgResponseTime = responseTimes.length > 0
|
|
72
|
+
? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length
|
|
73
|
+
: 0;
|
|
74
|
+
const duration = this.endTime - this.startTime;
|
|
75
|
+
const requestsPerSecond = duration > 0 ? (totalRequests / duration) * 1000 : 0;
|
|
76
|
+
// Aggregate worker-specific results
|
|
77
|
+
const workerStats = {};
|
|
78
|
+
for (const [workerAddress, workerResults] of this.workerResults) {
|
|
79
|
+
const workerRequests = workerResults.length;
|
|
80
|
+
const workerSuccessful = workerResults.filter(r => r.success).length;
|
|
81
|
+
const workerErrors = workerRequests - workerSuccessful;
|
|
82
|
+
const workerResponseTimes = workerResults.map(r => r.duration || r.response_size || 0);
|
|
83
|
+
const workerAvgResponseTime = workerResponseTimes.length > 0
|
|
84
|
+
? workerResponseTimes.reduce((sum, time) => sum + time, 0) / workerResponseTimes.length
|
|
85
|
+
: 0;
|
|
86
|
+
const workerRequestsPerSecond = duration > 0 ? (workerRequests / duration) * 1000 : 0;
|
|
87
|
+
workerStats[workerAddress] = {
|
|
88
|
+
requests: workerRequests,
|
|
89
|
+
errors: workerErrors,
|
|
90
|
+
avg_response_time: workerAvgResponseTime,
|
|
91
|
+
requests_per_second: workerRequestsPerSecond
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Calculate total virtual users across all workers
|
|
95
|
+
// Prefer VU ramp-up events if available (most accurate)
|
|
96
|
+
let totalVUs = 0;
|
|
97
|
+
if (this.vuRampUpEvents.length > 0) {
|
|
98
|
+
// Count from actual VU start events (already made globally unique)
|
|
99
|
+
totalVUs = this.vuRampUpEvents.length;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Fallback: Each worker has its own VU IDs (1, 2, 3...), so count per worker and sum
|
|
103
|
+
for (const [, workerResults] of this.workerResults) {
|
|
104
|
+
const uniqueVUsPerWorker = new Set(workerResults.map(r => r.vu_id)).size;
|
|
105
|
+
totalVUs += uniqueVUsPerWorker;
|
|
106
|
+
}
|
|
107
|
+
// Fallback if no worker info available
|
|
108
|
+
if (totalVUs === 0) {
|
|
109
|
+
totalVUs = new Set(this.results.map(r => r.vu_id)).size;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Duration in seconds for report display
|
|
113
|
+
const totalDurationSeconds = duration / 1000;
|
|
114
|
+
// Sort VU ramp-up events by start time for proper chart rendering
|
|
115
|
+
const sortedVURampUp = [...this.vuRampUpEvents].sort((a, b) => a.start_time - b.start_time);
|
|
116
|
+
return {
|
|
117
|
+
summary: {
|
|
118
|
+
total_requests: totalRequests,
|
|
119
|
+
success_rate: successRate,
|
|
120
|
+
avg_response_time: avgResponseTime,
|
|
121
|
+
requests_per_second: requestsPerSecond,
|
|
122
|
+
total_errors: failedRequests,
|
|
123
|
+
start_time: this.startTime,
|
|
124
|
+
end_time: this.endTime,
|
|
125
|
+
duration,
|
|
126
|
+
total_duration: totalDurationSeconds,
|
|
127
|
+
total_virtual_users: totalVUs,
|
|
128
|
+
peak_virtual_users: totalVUs,
|
|
129
|
+
successful_requests: successfulRequests,
|
|
130
|
+
failed_requests: failedRequests,
|
|
131
|
+
vu_ramp_up: sortedVURampUp
|
|
132
|
+
},
|
|
133
|
+
results: this.results,
|
|
134
|
+
workers: workerStats
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
clear() {
|
|
138
|
+
this.results = [];
|
|
139
|
+
this.workerResults.clear();
|
|
140
|
+
this.vuRampUpEvents = [];
|
|
141
|
+
this.workerVUEvents.clear();
|
|
142
|
+
this.startTime = 0;
|
|
143
|
+
this.endTime = 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.ResultAggregator = ResultAggregator;
|