UNPKG

loadly

Version:

Load testing CLI + React UI with live analytics

125 lines (113 loc) 3.96 kB
import crypto from "node:crypto"; import { performance } from "node:perf_hooks"; export function fillTemplate(value) { if (value == null) return value; const replaceInString = (s) => s .replace(/\{\{timestamp\}\}/g, String(Date.now())) .replace(/\{\{uuid\}\}/g, crypto.randomUUID()) .replace(/\{\{randint:(-?\d+),(-?\d+)\}\}/g, (_, a, b) => { const min = parseInt(a, 10), max = parseInt(b, 10); return String(Math.floor(Math.random() * (max - min + 1)) + min); }); if (typeof value === "string") return replaceInString(value); if (Array.isArray(value)) return value.map(fillTemplate); if (typeof value === "object") { const out = {}; for (const k of Object.keys(value)) out[k] = fillTemplate(value[k]); return out; } return value; } export function pickWeighted(endpoints) { const sum = endpoints.reduce((a, e) => a + (e.weight || 1), 0); let r = Math.random() * sum; for (const e of endpoints) { r -= e.weight || 1; if (r <= 0) return e; } return endpoints[endpoints.length - 1]; } export async function runLoadTest(config, onUpdate, abortSignal) { const { endpoints, headers, duration, concurrency } = config; const tStart = Date.now(); const tEnd = tStart + duration * 1000; const stats = { total: 0, success: 0, failure: 0, times: [], codes: {} }; const sendUpdate = () => { const elapsed = (Date.now() - tStart) / 1000; const count = stats.times.length || 1; const avg = stats.times.reduce((a, b) => a + b, 0) / count; const sorted = stats.times.slice().sort((a, b) => a - b); const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0; const mem = process.memoryUsage(); onUpdate({ total: stats.total, success: stats.success, failure: stats.failure, rps: stats.total / Math.max(elapsed, 0.001), avg, p95, memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal }, elapsedSec: elapsed, codes: stats.codes, }); }; async function oneRequest() { const ep = pickWeighted(endpoints); const finalHeaders = { ...(headers || {}), ...(ep.headers || {}) }; const body = ep.body !== undefined ? fillTemplate(ep.body) : undefined; const init = { method: ep.method || "GET", headers: finalHeaders, body: body !== undefined ? JSON.stringify(body) : undefined, }; const t0 = performance.now(); try { const res = await fetch(ep.url, init); const dt = performance.now() - t0; stats.times.push(dt); stats.total++; stats.codes[res.status] = (stats.codes[res.status] || 0) + 1; if (res.ok) stats.success++; else stats.failure++; } catch (e) { const dt = performance.now() - t0; stats.times.push(dt); stats.total++; stats.codes["ERR"] = (stats.codes["ERR"] || 0) + 1; stats.failure++; } } let lastUpdate = 0; async function worker() { while (Date.now() < tEnd && !(abortSignal?.aborted)) { await oneRequest(); const now = Date.now(); if (now - lastUpdate > 250) { lastUpdate = now; sendUpdate(); } } } await Promise.all(Array.from({ length: concurrency }, () => worker())); sendUpdate(); const elapsed = (Date.now() - tStart) / 1000; const avg = stats.times.length ? stats.times.reduce((a, b) => a + b, 0) / stats.times.length : 0; const sorted = stats.times.slice().sort((a, b) => a - b); const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0; const mem = process.memoryUsage(); return { total: stats.total, success: stats.success, failure: stats.failure, rps: stats.total / Math.max(elapsed, 0.001), avg, p95, memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal }, elapsedSec: elapsed, codes: stats.codes, }; } // helper export for testing export const _test = { fillTemplate, pickWeighted };