iostress
Version:
🚀 Blast your Socket.IO server with this quick and powerful JavaScript testing tool!
376 lines (368 loc) • 11.9 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2,
mod
));
// src/runner/test-runner.ts
var import_node_worker_threads = require("worker_threads");
// src/runner/client.ts
var import_socket = require("socket.io-client");
// src/utils.ts
var import_typescript = __toESM(require("typescript"));
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_kleur = __toESM(require("kleur"));
var import_events = __toESM(require("events"));
var Performance = class {
timers = {};
incrementorIdx = 0;
measure(timerId, keepTimer = false) {
if (!this.timers[timerId]) return -1;
const [seconds, nanoseconds] = process.hrtime(this.timers[timerId]);
const milliseconds = seconds * 1e3 + nanoseconds / 1e6;
if (!keepTimer) this.delete(timerId);
return milliseconds;
}
delete(timerId) {
if (this.timers[timerId]) delete this.timers[timerId];
}
start() {
this.timers[this.incrementorIdx] = process.hrtime();
return this.incrementorIdx++;
}
};
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
var random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
// src/runner/client.ts
var import_types = require("util/types");
var import_node_events = __toESM(require("events"));
// src/runner/logger.ts
var import_events2 = __toESM(require("events"));
var import_winston = require("winston");
var Logger = class extends import_events2.default {
winston;
constructor(logsDir2) {
super();
const { printf, combine, timestamp } = import_winston.format;
const fileFormat = printf(({ level, message, label, timestamp: timestamp2 }) => {
const spaces = " ".repeat(7 - level.length);
return `${timestamp2} ${spaces}${level.toUpperCase()} [${label}] ${message}`;
});
this.winston = (0, import_winston.createLogger)({
level: "debug",
format: combine(timestamp(), fileFormat),
transports: [
new import_winston.transports.File({
dirname: logsDir2,
filename: "iostress.error.log",
level: "error"
}),
new import_winston.transports.File({
dirname: logsDir2,
filename: "iostress.info.log",
level: "info"
}),
new import_winston.transports.File({
dirname: logsDir2,
filename: "iostress.combined.log"
})
]
});
}
log(message, type = "Business") {
this.winston.info(message, { label: type });
}
error(message, type = "Business") {
this.winston.error(
(typeof message === "string" ? new Error(message).stack : message.stack) ?? "Unknown error",
{ label: type }
);
this.emit("error", type);
}
warn(message, type = "Business") {
this.winston.warn(message, { label: type });
}
debug(message, type = "Business") {
this.winston.debug(message, { label: type });
}
};
// src/runner/client.ts
var Client = class extends import_node_events.default {
constructor(options) {
super();
this.options = options;
this.logger = new Logger(options.logsDir);
this.logger.on("error", (type) => {
this.report.errors.total++;
this.report.errors.byType[type] = (this.report.errors.byType[type] ?? 0) + 1;
});
const connTimer = this.performance.start();
this.socket = (0, import_socket.io)(this.options.target, options.initializer);
this.socket.on("connect", () => {
if (this.report.connection.latency === -1) {
this.report.connection.latency = this.performance.measure(
connTimer,
true
);
this.report.connection.success = true;
}
this.report.connection.attempted = true;
});
this.socket.on("connect_error", (error) => {
this.logger.error(error, "Connection");
if (!this.socket.active) {
this.emit("finished", this.report);
}
});
this.socket.on("reconnect_attempt", () => {
this.report.connection.reconnectAttempts++;
});
this.socket.on("reconnect", () => {
this.report.connection.reconnectSuccess++;
});
this.socket.on("reconnect_error", (error) => {
this.logger.error(error, "Connection");
});
const originalEmit = this.socket.emit.bind(this.socket);
this.socket.emit = (ev, ...args) => {
const emitTimer = this.performance.start();
const lastArg = args[args.length - 1];
if (typeof lastArg === "function") {
const originalAck = lastArg;
args[args.length - 1] = async (...ackArgs) => {
try {
const latency = this.performance.measure(emitTimer);
this.report.events.latencyFrames.push(latency);
let result;
if ((0, import_types.isPromise)(originalAck)) result = await originalAck(...ackArgs);
else result = originalAck(...ackArgs);
this.report.events.successful++;
return result;
} catch (error) {
this.report.events.failed++;
this.logger.error(error, "Business");
}
};
}
try {
const result = originalEmit(ev, ...args);
if (typeof lastArg !== "function") {
const latency = this.performance.measure(emitTimer);
this.report.events.latencyFrames.push(latency);
this.report.events.successful++;
}
return result;
} catch (error) {
this.report.events.failed++;
this.logger.error(error, "Business");
throw error;
}
};
this.socket.emitWithAck = (ev, ...args) => {
return new Promise((resolve, reject) => {
try {
this.socket.emit(ev, ...args, (result) => {
resolve(result);
});
} catch (error) {
reject(error);
}
});
};
this.socket.onAnyOutgoing(() => {
this.report.events.sent++;
});
this.socket.onAny(() => {
this.report.events.received++;
});
this.socket.on("disconnect", (reason) => {
if (reason === "io server disconnect" || reason === "io client disconnect") {
this.emit("finished", this.report);
}
});
this.on("SIGTERM", () => {
if (this.socket.connected) {
this.socket.disconnect();
} else {
this.socket.off();
this.socket.offAny();
this.socket.offAnyOutgoing();
this.socket.disconnect();
this.socket.close();
this.emit("finished", this.report);
}
});
}
socket;
report = {
connection: {
attempted: false,
latency: -1,
success: false,
reconnectAttempts: 0,
reconnectSuccess: 0
},
errors: {
total: 0,
byType: {}
},
events: {
sent: 0,
received: 0,
successful: 0,
failed: 0,
latencyFrames: []
}
};
performance = new Performance();
logger;
async runTest() {
let timeout;
try {
let fn = (await import(this.options.scenarioPath)).default;
if (typeof fn !== "function") fn = fn.default;
if (this.options.scenarioTimeout) {
timeout = setTimeout(() => {
this.socket.disconnect();
}, this.options.scenarioTimeout);
}
fn(this.socket, this.logger);
this.emit("running");
} catch (error) {
if (timeout) clearTimeout(timeout);
this.logger.error(error, "Business");
this.socket.disconnect();
}
}
};
// src/runner/test-runner.ts
var {
target,
starterInitializers,
finalInitializers,
rampDelayRate,
scenarioPath,
scenarioTimeout,
logsDir
} = import_node_worker_threads.workerData;
var runnerReport = {
connections: {
attempted: 0,
successful: 0,
failed: 0,
reconnectAttempts: 0,
reconnectSuccess: 0,
latencyFrames: []
},
errors: {
total: 0,
byType: {}
},
events: {
sent: 0,
received: 0,
successful: 0,
failed: 0,
latencyFrames: []
}
};
var clientsCount = starterInitializers.length + finalInitializers.length;
var readyClients = 0;
var runningClients = 0;
var finishedClients = 0;
var terminators = [];
var runPhaseSlice = async () => {
let lazy = false;
for (const initializer of [...starterInitializers, ...finalInitializers]) {
if (lazy) await sleep(random(1, 10) * rampDelayRate);
readyClients++;
if (!lazy && readyClients >= starterInitializers.length) {
lazy = true;
}
const client = new Client({
target,
scenarioPath,
scenarioTimeout,
logsDir,
initializer
});
client.on("running", () => {
readyClients--;
runningClients++;
import_node_worker_threads.parentPort?.postMessage({
event: "status",
data: {
readyClients,
runningClients,
finishedClients
}
});
});
client.on("finished", (report) => {
runningClients--;
finishedClients++;
import_node_worker_threads.parentPort?.postMessage({
event: "status",
data: {
readyClients,
runningClients,
finishedClients
}
});
if (report.connection.attempted) runnerReport.connections.attempted++;
if (report.connection.success) runnerReport.connections.successful++;
else runnerReport.connections.failed++;
runnerReport.connections.reconnectAttempts += report.connection.reconnectAttempts;
runnerReport.connections.reconnectSuccess += report.connection.reconnectSuccess;
runnerReport.connections.latencyFrames.push(report.connection.latency);
runnerReport.errors.total += report.errors.total;
for (const errorType in report.errors.byType) {
if (runnerReport.errors.byType[errorType]) {
runnerReport.errors.byType[errorType] += report.errors.byType[errorType];
} else {
runnerReport.errors.byType[errorType] = report.errors.byType[errorType];
}
}
runnerReport.events.sent += report.events.sent;
runnerReport.events.received += report.events.received;
runnerReport.events.successful += report.events.successful;
runnerReport.events.failed += report.events.failed;
runnerReport.events.latencyFrames.push(...report.events.latencyFrames);
if (clientsCount === finishedClients) {
import_node_worker_threads.parentPort?.postMessage({
event: "finished",
report: runnerReport
});
process.exit(0);
}
});
terminators.push(() => {
client.emit("SIGTERM");
});
client.runTest();
}
import_node_worker_threads.parentPort?.on("message", (message) => {
if (message.event === "SIGTERM") {
terminators.forEach((terminator) => terminator());
}
});
};
runPhaseSlice();