Files
thePR0M3TH3ANandClaude Opus 4.8 2c657c961e test(perf): add fresh-context watch-history decrypt repro
wh-repro.mjs uses a SEPARATE browser context per session (seed vs cold), so the
persisted decrypt cache doesn't warm between runs, and captures the page-side
stack of every window.nostr.nip44.decrypt call.

It root-caused the watch-history cold-load cost: history is bucketed by month
(one encrypted kind-30079 snapshot per month) and the load decrypts EVERY month
via the extension — 600 items / 18 months => 18 serialized decrypt calls. So the
cost is ~1 nip-07 decrypt per month of history; a multi-year history is the ~20s
cold load. (The earlier "~180" figure was a shared-context harness artifact.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-12 20:55:22 -04:00

130 lines
6.1 KiB
JavaScript

// Focused watch-history decrypt reproduction with FRESH contexts per session
// (so the persisted decrypt cache doesn't warm between cold/warm runs), and
// page-side capture of where window.nostr.nip44.decrypt is actually called from.
//
// Usage: node scripts/perf/wh-repro.mjs (env: WH_ITEMS=200)
import { chromium } from "playwright";
import { startRelay } from "../agent/simple-relay.mjs";
import { finalizeEvent, getPublicKey } from "nostr-tools";
import * as nip44 from "nostr-tools/nip44";
const APP = process.env.APP || "http://localhost:3000";
const WH_ITEMS = Number(process.env.WH_ITEMS ?? 200);
const WS_PORT = 8963;
const USER_SK = Uint8Array.from(Buffer.from("11".repeat(32), "hex"));
const USER_PK = getPublicKey(USER_SK);
let calls = 0;
const cts = new Map(); // ciphertext -> count
const convKey = (pk) => nip44.v2.utils.getConversationKey(USER_SK, pk);
async function setupContext(browser, relayUrl) {
const context = await browser.newContext();
await context.exposeBinding("__gpk", () => USER_PK);
await context.exposeBinding("__sign", (_s, e) => finalizeEvent({ ...e, pubkey: USER_PK }, USER_SK));
await context.exposeBinding("__n44e", (_s, pk, t) => nip44.v2.encrypt(t, convKey(pk)));
await context.exposeBinding("__n44d", (_s, pk, ct) => {
calls += 1;
cts.set(ct, (cts.get(ct) || 0) + 1);
return nip44.v2.decrypt(ct, convKey(pk));
});
await context.exposeBinding("__stack", (_s, s) => {
// log only the first few distinct stacks
if (setupContext._stacks === undefined) setupContext._stacks = new Set();
if (setupContext._stacks.size < 4) {
setupContext._stacks.add(s);
console.log("STACK>", s);
}
});
await context.addInitScript((url) => {
localStorage.setItem("hasSeenDisclaimer", "true");
localStorage.setItem("__bitvidTestMode__", "1");
localStorage.setItem("__bitvidTestRelays__", JSON.stringify([url]));
localStorage.setItem("bitvid_admin_whitelist_mode", "false");
const w = (n) => (...a) => window[n](...a);
const n44d = (p, c) => {
try { window.__stack((new Error().stack || "").split("\n").slice(1, 5).join(" <- ")); } catch (_) {}
return w("__n44d")(p, c);
};
window.nostr = {
getPublicKey: () => w("__gpk")(),
signEvent: (e) => w("__sign")(e),
getRelays: async () => ({}),
nip04: { encrypt: async () => "", decrypt: async () => "" },
nip44: { encrypt: (p, t) => w("__n44e")(p, t), decrypt: n44d, v2: { encrypt: (p, t) => w("__n44e")(p, t), decrypt: n44d } },
};
}, relayUrl);
return context;
}
async function login(context, relayUrl) {
const page = await context.newPage();
await page.goto(`${APP}/?__test__=1&__testRelays__=${encodeURIComponent(relayUrl)}`, { waitUntil: "domcontentloaded" });
await page.waitForFunction(() => typeof window.__bitvidTest__ === "object", { timeout: 20000 }).catch(() => {});
await page.evaluate((u) => window.__bitvidTest__?.setTestRelays?.([u], { persist: false }), relayUrl).catch(() => {});
try {
await page.click('[data-testid="login-button"]', { timeout: 8000 });
await page.waitForSelector('[data-testid="login-modal"]', { timeout: 8000 });
const b = page.getByRole("button", { name: /extension|nip-?07|browser/i }).first();
if (await b.count()) await b.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;
}
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));
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
// Context A: publish a watch history of WH_ITEMS pointers.
const ctxA = await setupContext(browser, relayUrl);
const pA = await login(ctxA, relayUrl);
const hex = (i) => (i.toString(16).padStart(8, "0") + "a".repeat(56)).slice(0, 64);
const MONTHS = Number(process.env.MONTHS ?? 1);
// Spread items across MONTHS distinct months to exercise the monthly bucketing.
const items = Array.from({ length: WH_ITEMS }, (_, i) => ({
type: "e",
value: hex(i + 1),
watchedAt: now - (i % MONTHS) * 31 * 86400 - i * 30,
}));
const pub = await pA.evaluate(async (its) => {
const c = window.__bitvidTest__?.nostrClient;
try { return { ok: true, r: await c.publishWatchHistorySnapshot(its, {}) }; } catch (e) { return { ok: false, e: String(e).split("\n")[0] }; }
}, items);
await pA.waitForTimeout(1500);
const byKind = {};
for (const e of relay.getEvents().values()) byKind[e.kind] = (byKind[e.kind] || 0) + 1;
await ctxA.close();
// Context B (FRESH = truly cold): resolve watch history, count decrypts.
calls = 0; cts.clear();
const ctxB = await setupContext(browser, relayUrl);
const pB = await login(ctxB, relayUrl);
const cold = await pB.evaluate(async (pk) => {
const c = window.__bitvidTest__?.nostrClient;
try { const it = await c.resolveWatchHistory(pk, { forceRefresh: true }); return { ok: true, items: it?.length || 0 }; } catch (e) { return { ok: false, e: String(e).split("\n")[0] }; }
}, USER_PK);
await pB.waitForTimeout(1500);
const coldCalls = calls, coldUnique = cts.size, coldMax = Math.max(0, ...[...cts.values()]);
await ctxB.close();
console.log("\n================ WH REPRO ================");
console.log(`publish: ${JSON.stringify(pub)} (${WH_ITEMS} items)`);
console.log(`relay events by kind: ${JSON.stringify(byKind)}`);
console.log(`COLD resolve: ${JSON.stringify(cold)}`);
console.log(`COLD nip44.decrypt calls=${coldCalls} uniqueCiphertexts=${coldUnique} maxRepeat=${coldMax}`);
console.log("=========================================\n");
await browser.close();
if (typeof relay.close === "function") await relay.close();
process.exit(0);
}
main().catch((e) => { console.error("wh-repro failed:", e); process.exit(1); });