bench: Multi-temperature cloud bench
Some checks failed
CI / Test (OTP 27.2 / Elixir 1.18.2) (push) Failing after 0s
CI / Test (OTP 28.4 / Elixir 1.19.4 + E2E) (push) Failing after 0s

This commit is contained in:
2026-03-19 22:14:35 +01:00
parent c45dbadd78
commit e02bd99a43
7 changed files with 1275 additions and 247 deletions

185
scripts/nostr_seed.mjs Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env node
// Nostr event seeder — generates and publishes events matching nostr-bench
// patterns to a relay via WebSocket.
//
// Standalone:
// node scripts/nostr_seed.mjs --url ws://127.0.0.1:4413/relay --count 10000 [--concurrency 8]
//
// As module:
// import { seedEvents } from './nostr_seed.mjs';
// const result = await seedEvents({ url, count, concurrency: 8 });
import { generateSecretKey, getPublicKey, finalizeEvent } from "nostr-tools/pure";
import WebSocket from "ws";
import { parseArgs } from "node:util";
import { fileURLToPath } from "node:url";
// Matches nostr-bench util.rs:48-66 exactly.
function generateBenchEvent() {
const sk = generateSecretKey();
const pk = getPublicKey(sk);
const benchTag = `nostr-bench-${Math.floor(Math.random() * 1000)}`;
return finalizeEvent(
{
kind: 1,
created_at: Math.floor(Date.now() / 1000),
content: "This is a message from nostr-bench client",
tags: [
["p", pk],
[
"e",
"378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7",
],
["t", "nostr-bench-"],
["t", benchTag],
],
},
sk,
);
}
/**
* Seed exactly `count` events to a relay.
*
* Opens `concurrency` parallel WebSocket connections, generates events
* matching nostr-bench patterns, sends them, and waits for OK acks.
*
* @param {object} opts
* @param {string} opts.url Relay WebSocket URL
* @param {number} opts.count Number of events to send
* @param {number} [opts.concurrency] Parallel connections (default 8)
* @param {function} [opts.onProgress] Called with (acked_so_far) periodically
* @returns {Promise<{sent: number, acked: number, errors: number, elapsed_ms: number}>}
*/
export async function seedEvents({ url, count, concurrency = 8, onProgress }) {
if (count <= 0) return { sent: 0, acked: 0, errors: 0, elapsed_ms: 0 };
const startMs = Date.now();
let sent = 0;
let acked = 0;
let errors = 0;
let nextToSend = 0;
// Claim the next event index atomically (single-threaded, but clear intent).
function claimNext() {
if (nextToSend >= count) return -1;
return nextToSend++;
}
async function runWorker() {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url);
let pendingResolve = null;
let closed = false;
ws.on("error", (err) => {
if (!closed) {
closed = true;
reject(err);
}
});
ws.on("close", () => {
closed = true;
if (pendingResolve) pendingResolve();
resolve();
});
ws.on("open", () => {
sendNext();
});
ws.on("message", (data) => {
const msg = data.toString();
if (msg.includes('"OK"')) {
if (msg.includes("true")) {
acked++;
} else {
errors++;
}
if (onProgress && (acked + errors) % 10000 === 0) {
onProgress(acked);
}
sendNext();
}
});
function sendNext() {
if (closed) return;
const idx = claimNext();
if (idx < 0) {
closed = true;
ws.close();
return;
}
const event = generateBenchEvent();
sent++;
ws.send(JSON.stringify(["EVENT", event]));
}
});
}
const workers = [];
const actualConcurrency = Math.min(concurrency, count);
for (let i = 0; i < actualConcurrency; i++) {
workers.push(runWorker());
}
await Promise.allSettled(workers);
return {
sent,
acked,
errors,
elapsed_ms: Date.now() - startMs,
};
}
// CLI entrypoint
const isMain =
process.argv[1] &&
fileURLToPath(import.meta.url).endsWith(process.argv[1].replace(/^.*\//, ""));
if (isMain) {
const { values } = parseArgs({
options: {
url: { type: "string" },
count: { type: "string" },
concurrency: { type: "string", default: "8" },
},
strict: false,
});
if (!values.url || !values.count) {
console.error(
"usage: node scripts/nostr_seed.mjs --url ws://... --count <n> [--concurrency <n>]",
);
process.exit(1);
}
const count = Number(values.count);
const concurrency = Number(values.concurrency);
if (!Number.isInteger(count) || count < 1) {
console.error("--count must be a positive integer");
process.exit(1);
}
console.log(
`[seed] seeding ${count} events to ${values.url} (concurrency=${concurrency})`,
);
const result = await seedEvents({
url: values.url,
count,
concurrency,
onProgress: (n) => {
process.stdout.write(`\r[seed] ${n}/${count} acked`);
},
});
if (count > 500) process.stdout.write("\n");
console.log(JSON.stringify(result));
}