mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
This commit is contained in:
1 parent
3d0564feea
commit
2c657c961e
1 file changed
+129
@@ -0,0 +1,129 @@
|
||||
// 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); });
|
||||
Reference in new issue
Block a user