UNPKG

iostress

Version:

🚀 Blast your Socket.IO server with this quick and powerful JavaScript testing tool!

376 lines (368 loc) • 11.9 kB
"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();