import { setTimeout as sleep } from "node:timers/promises";
import { performance } from "node:perf_hooks";
const args = process.argv.slice(2);
const getArg = (name, fallback) => {
const idx = args.indexOf(`--${name}`);
if (idx === -1 || idx + 1 >= args.length) return fallback;
return args[idx + 1];
};
const baseUrl = getArg("base", "http://localhost:8090/");
const workers = Number.parseInt(getArg("workers", "50"), 10);
const steps = Number.parseInt(getArg("steps", "200"), 10);
const minDelay = Number.parseInt(getArg("minDelay", "200"), 10);
const maxDelay = Number.parseInt(getArg("maxDelay", "2000"), 10);
const timeout = Number.parseInt(getArg("timeout", "8000"), 10);
const reportEvery = Number.parseInt(getArg("reportEvery", "50"), 10);
const progressEvery = Number.parseInt(getArg("progressEvery", "0"), 10);
const maxPool = Number.parseInt(getArg("maxPool", "500"), 10);
const maxTextBytes = Number.parseInt(getArg("maxTextBytes", "1048576"), 10);
const maxConcurrent = Number.parseInt(getArg("maxConcurrent", "6"), 10);
const minConcurrent = Number.parseInt(getArg("minConcurrent", "2"), 10);
const dropChance = Number.parseFloat(getArg("dropChance", "0.05"));
const concurrencyJitterSteps = Number.parseInt(getArg("concurrencyJitterSteps", "25"), 10);
const fetchImages = getArg("fetchImages", "1") !== "0";
const imageChance = Number.parseFloat(getArg("imageChance", "0.25"));
const maxImagePool = Number.parseInt(getArg("maxImagePool", "500"), 10);
const fetchAssets = getArg("fetchAssets", "1") !== "0";
const assetChance = Number.parseFloat(getArg("assetChance", "0.2"));
const maxAssetPool = Number.parseInt(getArg("maxAssetPool", "500"), 10);
const warmupMs = Number.parseInt(getArg("warmupMs", "15000"), 10);
const warmupConcurrent = Number.parseInt(getArg("warmupConcurrent", "1"), 10);
const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const sleepRand = () => sleep(rand(minDelay, maxDelay));
const pool = [];
const poolSet = new Set();
const imagePool = [];
const imagePoolSet = new Set();
const assetPool = [];
const assetPoolSet = new Set();
const isSafeHref = (href) => {
if (!href) return false;
const raw = href.trim();
if (!raw || raw.startsWith("#")) return false;
if (raw.startsWith("mailto:")) return false;
if (raw.startsWith("javascript:")) return false;
return true;
};
const isSafePath = (path) => {
const p = path.toLowerCase();
if (p.includes("logout") || p.includes("delete") || p.includes("/login")) return false;
return true;
};
const addToPool = (url) => {
if (poolSet.has(url)) return;
if (pool.length >= maxPool) return;
poolSet.add(url);
pool.push(url);
};
const addToImagePool = (url) => {
if (imagePoolSet.has(url)) return;
if (imagePool.length >= maxImagePool) return;
imagePoolSet.add(url);
imagePool.push(url);
};
const addToAssetPool = (url) => {
if (assetPoolSet.has(url)) return;
if (assetPool.length >= maxAssetPool) return;
assetPoolSet.add(url);
assetPool.push(url);
};
const pickFromPool = () => {
if (!pool.length) return baseUrl;
return pool[rand(0, pool.length - 1)];
};
const pickImage = () => {
if (!imagePool.length) return null;
return imagePool[rand(0, imagePool.length - 1)];
};
const pickAsset = () => {
if (!assetPool.length) return null;
return assetPool[rand(0, assetPool.length - 1)];
};
const extractLinks = (html, origin, base) => {
const links = [];
const images = [];
const assets = [];
const re = /href\s*=\s*["']([^"']+)["']/gi;
let match;
while ((match = re.exec(html)) !== null) {
const href = match[1];
if (!isSafeHref(href)) continue;
try {
const url = new URL(href, base);
if (url.origin !== origin) continue;
const path = `${url.pathname}${url.search}`;
if (!isSafePath(path)) continue;
links.push(url.toString());
} catch {
// ignore bad urls
}
}
const imgRe = /
]+src\s*=\s*["']([^"']+)["']/gi;
while ((match = imgRe.exec(html)) !== null) {
const src = match[1];
if (!isSafeHref(src)) continue;
try {
const url = new URL(src, base);
if (url.origin !== origin) continue;
images.push(url.toString());
} catch {
// ignore bad urls
}
}
const scriptRe = /