mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Headless Playwright probes that reproduce the real login path via a fake window.nostr extension (which loginWithNsec bypasses) and measure steady-state extension-call and relay-REQ rates: - nip07-heat.mjs, req-capture.mjs, ws-lifecycle.mjs, loopc-capture.mjs, etc. Plus js/devReqMonitor.js: a localhost-only WebSocket REQ-rate monitor that warns in-console when relay traffic spikes, naming the storming kinds — used to capture the real-environment storm the mock-relay harness could not see. Removable once the refactor lands. Co-Authored-By: Claude Opus 4.8 <[email protected]>
187 lines
9.2 KiB
JavaScript
187 lines
9.2 KiB
JavaScript
// nip-07 login HEAT probe.
|
|
//
|
|
// Reproduces the *real* nip-07 login path (which loginWithNsec skips) using a
|
|
// fake window.nostr extension backed by Node-side nostr-tools crypto, then
|
|
// measures STEADY-STATE activity after login settles:
|
|
// - extension calls/sec by method (getPublicKey/signEvent/nip04/nip44)
|
|
// - outgoing relay REQ frames/sec
|
|
// A non-trivial steady-state rate = the login heat (a loop re-decrypting /
|
|
// re-subscribing after the feed should be idle).
|
|
//
|
|
// Usage: APP=http://localhost:3000 WINDOW=15 LATENCY_MS=0 node scripts/perf/nip07-heat.mjs
|
|
|
|
import { chromium } from "playwright";
|
|
import { startRelay } from "../agent/simple-relay.mjs";
|
|
import { finalizeEvent, getPublicKey, nip04 } from "nostr-tools";
|
|
import * as nip44 from "nostr-tools/nip44";
|
|
|
|
const APP = process.env.APP || "http://localhost:3000";
|
|
const LATENCY_MS = Number(process.env.LATENCY_MS ?? 0);
|
|
const WINDOW = Number(process.env.WINDOW ?? 15);
|
|
const VIDEOS = Number(process.env.VIDEOS ?? 30);
|
|
const WH_ITEMS = Number(process.env.WH_ITEMS ?? 60);
|
|
const WS_PORT = 8973;
|
|
|
|
const USER_SK = Uint8Array.from(Buffer.from("11".repeat(32), "hex"));
|
|
const USER_PK = getPublicKey(USER_SK);
|
|
|
|
let extCalls = {};
|
|
let queue = Promise.resolve();
|
|
const reset = () => { extCalls = {}; };
|
|
function viaExtension(method, fn) {
|
|
extCalls[method] = (extCalls[method] || 0) + 1;
|
|
const run = async () => {
|
|
if (LATENCY_MS > 0) await new Promise((r) => setTimeout(r, LATENCY_MS));
|
|
return fn();
|
|
};
|
|
const p = queue.then(run, run);
|
|
queue = p.catch(() => {});
|
|
return p;
|
|
}
|
|
const convKey = (peerPk) => nip44.v2.utils.getConversationKey(USER_SK, peerPk);
|
|
|
|
let reqCount = 0;
|
|
let measuring = false;
|
|
|
|
async function main() {
|
|
const relay = startRelay(WS_PORT, { httpPort: false });
|
|
const relayUrl = `ws://127.0.0.1:${WS_PORT}`;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
relay.seedEvent(finalizeEvent({ kind: 10002, created_at: now, tags: [["r", relayUrl]], content: "" }, USER_SK));
|
|
for (let i = 0; i < VIDEOS; i++) {
|
|
const dTag = `seed-${i}`;
|
|
relay.seedEvent(finalizeEvent({
|
|
kind: 30078,
|
|
created_at: now - i * 60,
|
|
tags: [["d", dTag], ["t", "video"], ["title", `Heat Video ${i}`], ["url", `https://example.com/v${i}.mp4`]],
|
|
content: JSON.stringify({ version: 3, title: `Heat Video ${i}`, videoRootId: dTag, mode: "live", isPrivate: false, deleted: false, url: `https://example.com/v${i}.mp4` }),
|
|
}, USER_SK));
|
|
}
|
|
|
|
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
|
|
const context = await browser.newContext();
|
|
|
|
await context.exposeBinding("__extGetPublicKey", () => viaExtension("getPublicKey", () => USER_PK));
|
|
await context.exposeBinding("__extSignEvent", (_s, e) => viaExtension("signEvent", () => finalizeEvent({ ...e, pubkey: USER_PK }, USER_SK)));
|
|
await context.exposeBinding("__extNip04Encrypt", (_s, pk, t) => viaExtension("nip04.encrypt", () => nip04.encrypt(USER_SK, pk, t)));
|
|
await context.exposeBinding("__extNip04Decrypt", (_s, pk, ct) => viaExtension("nip04.decrypt", () => nip04.decrypt(USER_SK, pk, ct)));
|
|
await context.exposeBinding("__extNip44Encrypt", (_s, pk, t) => viaExtension("nip44.encrypt", () => nip44.v2.encrypt(t, convKey(pk))));
|
|
await context.exposeBinding("__extNip44Decrypt", (_s, pk, ct) => viaExtension("nip44.decrypt", () => nip44.v2.decrypt(ct, convKey(pk))));
|
|
|
|
await context.addInitScript((url) => {
|
|
localStorage.setItem("hasSeenDisclaimer", "true");
|
|
localStorage.setItem("__bitvidTestMode__", "1");
|
|
localStorage.setItem("__bitvidTestRelays__", JSON.stringify([url]));
|
|
window.__bitvidTestRelays__ = [url];
|
|
const w = (n) => (...a) => window[n](...a);
|
|
window.nostr = {
|
|
getPublicKey: () => w("__extGetPublicKey")(),
|
|
signEvent: (e) => w("__extSignEvent")(e),
|
|
getRelays: async () => ({}),
|
|
nip04: { encrypt: (p, t) => w("__extNip04Encrypt")(p, t), decrypt: (p, c) => w("__extNip04Decrypt")(p, c) },
|
|
nip44: {
|
|
encrypt: (p, t) => w("__extNip44Encrypt")(p, t),
|
|
decrypt: (p, c) => w("__extNip44Decrypt")(p, c),
|
|
v2: { encrypt: (p, t) => w("__extNip44Encrypt")(p, t), decrypt: (p, c) => w("__extNip44Decrypt")(p, c) },
|
|
},
|
|
};
|
|
}, relayUrl);
|
|
|
|
const testUrl = `${APP}/?__test__=1&__testRelays__=${encodeURIComponent(relayUrl)}`;
|
|
|
|
async function session() {
|
|
const page = await context.newPage();
|
|
page.on("websocket", (ws) => {
|
|
ws.on("framesent", (d) => {
|
|
const p = typeof d.payload === "string" ? d.payload : "";
|
|
if (p.startsWith('["REQ"')) globalThis.__totalReq = (globalThis.__totalReq || 0) + 1;
|
|
if (measuring && p.startsWith('["REQ"')) {
|
|
reqCount += 1;
|
|
try {
|
|
const msg = JSON.parse(p);
|
|
const sig = msg.slice(2).map((f) => {
|
|
const k = f.kinds ? `kinds=${f.kinds.join(",")}` : "";
|
|
const keys = Object.keys(f).filter((x) => x !== "kinds").join(",");
|
|
return `${k} ${keys}`.trim();
|
|
}).join(" | ");
|
|
globalThis.__reqSig = globalThis.__reqSig || new Map();
|
|
globalThis.__reqSig.set(sig, (globalThis.__reqSig.get(sig) || 0) + 1);
|
|
} catch (_) {}
|
|
}
|
|
});
|
|
});
|
|
await page.goto(testUrl, { waitUntil: "domcontentloaded" });
|
|
await page.waitForFunction(() => typeof window.__bitvidTest__ === "object", { timeout: 20000 }).catch(() => {});
|
|
await page.evaluate((url) => window.__bitvidTest__?.setTestRelays?.([url], { persist: false }), relayUrl).catch(() => {});
|
|
await page.evaluate(() => {
|
|
const c = window.__bitvidTest__?.nostrClient;
|
|
if (c) c.videoEventVerifier = async (events) => new Set((events || []).map((e) => e && e.id).filter(Boolean));
|
|
}).catch(() => {});
|
|
try {
|
|
await page.click('[data-testid="login-button"]', { timeout: 8000 });
|
|
await page.waitForSelector('[data-testid="login-modal"]', { timeout: 8000 });
|
|
const extBtn = page.getByRole("button", { name: /extension|nip-?07|browser/i }).first();
|
|
if (await extBtn.count()) await extBtn.click({ timeout: 5000 });
|
|
else await page.locator('[data-testid="login-provider-button"]').first().click({ timeout: 5000 });
|
|
} catch (_) {}
|
|
await page.waitForFunction(async () => (await window.__bitvidTest__?.getAppState?.())?.isLoggedIn === true, { timeout: 15000 }).catch(() => {});
|
|
return page;
|
|
}
|
|
|
|
// Session 1: log in and publish a watch history (an encrypted list to decrypt).
|
|
const s1 = await session();
|
|
if (WH_ITEMS > 0) {
|
|
const hex = (i) => (i.toString(16).padStart(8, "0") + "a".repeat(56)).slice(0, 64);
|
|
const items = Array.from({ length: WH_ITEMS }, (_, i) => ({ type: "e", value: hex(i + 1), watchedAt: now - i * 30 }));
|
|
await s1.evaluate(async (its) => {
|
|
const c = window.__bitvidTest__?.nostrClient;
|
|
try { await c.publishWatchHistorySnapshot(its, {}); } catch (_) {}
|
|
}, items).catch(() => {});
|
|
await s1.waitForTimeout(1500);
|
|
}
|
|
await s1.close();
|
|
|
|
// Session 2: fresh login, let it settle, then measure STEADY-STATE activity.
|
|
globalThis.__totalReq = 0;
|
|
const page = await session();
|
|
await page.waitForTimeout(6000); // settle window (initial loads/decrypts)
|
|
const burstReq = globalThis.__totalReq || 0;
|
|
reset();
|
|
reqCount = 0;
|
|
await page.evaluate(() => { globalThis.__loopc = {}; }).catch(() => {});
|
|
measuring = true;
|
|
const start = Date.now();
|
|
await page.waitForTimeout(WINDOW * 1000);
|
|
const elapsed = (Date.now() - start) / 1000;
|
|
measuring = false;
|
|
const steady = { ...extCalls };
|
|
const loopc = await page.evaluate(() => globalThis.__loopc || {}).catch(() => ({}));
|
|
await page.close();
|
|
console.log("\nInstrumented call counts in window:");
|
|
for (const [k, n] of Object.entries(loopc).sort((a, b) => b[1] - a[1])) console.log(` ${k.padEnd(26)} ${String(n).padStart(6)} (${(n / elapsed).toFixed(1)}/s)`);
|
|
|
|
console.log("\n================ NIP-07 LOGIN HEAT ================");
|
|
console.log(`Initial load+settle burst: ${burstReq} REQ frames (first ~6s after login)\n`);
|
|
console.log(`Steady-state window: ${elapsed.toFixed(1)}s (post-login-settle)\n`);
|
|
console.log("Extension calls in window (method: count → rate):");
|
|
const entries = Object.entries(steady).sort((a, b) => b[1] - a[1]);
|
|
if (!entries.length) console.log(" (none — extension idle ✓)");
|
|
for (const [m, n] of entries) console.log(` ${m.padEnd(16)} ${String(n).padStart(5)} (${(n / elapsed).toFixed(1)}/s)`);
|
|
const totalExt = entries.reduce((s, [, n]) => s + n, 0);
|
|
console.log(`\nTotal extension calls/s: ${(totalExt / elapsed).toFixed(1)}`);
|
|
console.log(`Outgoing REQ frames/s : ${(reqCount / elapsed).toFixed(1)}`);
|
|
const sigs = [...(globalThis.__reqSig || new Map()).entries()].sort((a, b) => b[1] - a[1]);
|
|
console.log("\nTop REQ filters (count → rate → filter):");
|
|
for (const [sig, n] of sigs.slice(0, 10)) console.log(` ${String(n).padStart(5)} (${(n / elapsed).toFixed(1)}/s) ${sig}`);
|
|
console.log("\nInterpretation: a healthy idle login should be ~0 ext calls/s.");
|
|
console.log("Any steady nip04/nip44.decrypt rate = a re-decrypt loop (the heat).");
|
|
console.log("==================================================\n");
|
|
|
|
await browser.close();
|
|
if (typeof relay.close === "function") await relay.close();
|
|
process.exit(0);
|
|
}
|
|
|
|
main().catch((e) => { console.error("nip07-heat failed:", e); process.exit(1); });
|