Files
bitgate/tests/performance/large-feed.test.js
T
thePR0M3TH3AN c4477ae84b perf: memoize snapshot and fingerprint; add performance and security fixtures
The performance fixture the plan asks for found a real defect on its first run:
evaluation was quadratic in state size. Both the snapshot and its fingerprint
were rebuilt for every target, so each of 5,000 evaluations walked every report
and every mute list. A 5,000-target pass took ~47 seconds.

- the runtime memoizes the materialized snapshot and its fingerprint until a
  store reports a change
- evaluateMany computes the fingerprint once and shares it across the batch
- decisions are cached per (profile, target); report and mute changes
  invalidate only the targets they touch, while administrative, trust, policy,
  and viewer changes drop the whole cache
- cached decisions are deeply frozen, since the cache hands the same object to
  every caller

Same pass now takes ~85ms cold, ~6ms warm.

Adds tests/performance (5,000 targets, 500 authors, 100 muters per author) and
tests/security (one case per enumerated requirement testable at this layer).

605 tests; lint, typecheck, and the declaration build pass from a cold checkout.
2026-08-09 18:07:23 -04:00

298 lines
10 KiB
JavaScript

// Performance fixture.
//
// The extraction must not regress to per-item store lookups or per-card
// subscriptions. These assertions are structural rather than wall-clock: they
// check that no network call happens, that invalidation is targeted, and that
// teardown leaves nothing running. A timing assertion is included but kept
// deliberately loose, because a CI runner's clock is not a benchmark.
import { describe, expect, it, vi } from "vitest";
import { createPolicyDefinition } from "@nostr-governance/core";
import { createGovernanceRuntime, createMemoryTransport } from "@nostr-governance/runtime";
const AUTHOR_COUNT = 500;
const TARGET_COUNT = 5_000;
const MUTERS_PER_AUTHOR = 100;
const NOW = 1_750_000_000;
const hex = (prefix, index) => `${prefix}${index.toString(16).padStart(10, "0")}`.padEnd(64, "0").slice(0, 64);
/** @param {number} index @returns {import('@nostr-governance/core').GovernanceTarget} */
const userTarget = (index) => ({ type: "user", pubkey: hex("a", index) });
/** @param {number} index @param {number} authorIndex @returns {import('@nostr-governance/core').GovernanceTarget} */
const eventTarget = (index, authorIndex) => ({
type: "event",
id: hex("c", index),
author: hex("a", authorIndex),
});
const author = (index) => hex("a", index);
const muter = (index) => hex("b", index);
const eventId = (index) => hex("c", index);
const POLICY = createPolicyDefinition({
id: "perf",
version: "1.0.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
reports: { spam: { downrank: 1, hide: 8 }, scam: { restrict: 3 }, default: { downrank: 2 } },
mutes: { default: { downrank: 1, hide: 20 } },
muteWindowSeconds: 60 * 24 * 60 * 60,
},
strict: {
name: "strict",
administrativeDeny: { visibility: "deny", interaction: "deny" },
reports: { default: { hide: 1 } },
mutes: {},
},
},
});
/** Build a runtime loaded with a large, realistic governance state. */
function buildLargeRuntime() {
const transport = createMemoryTransport();
const runtime = createGovernanceRuntime({
applicationId: "perf",
namespace: "perf",
transport,
policy: POLICY,
now: () => NOW,
});
const ROOT = hex("f", 1);
const MODERATORS = [hex("d", 1), hex("d", 2), hex("d", 3)];
runtime.setViewer(hex("e", 1));
runtime.admin.setRoles({
root: ROOT,
actors: {
[ROOT]: ["super_admin"],
...Object.fromEntries(MODERATORS.map((pubkey) => [pubkey, ["moderator"]])),
},
});
// Several administrative contributors, each denying a slice of authors.
MODERATORS.forEach((actor, index) => {
runtime.admin.upsertContribution({
actor,
kind: "user-deny",
createdAt: NOW,
targets: Array.from({ length: 10 }, (_, offset) => userTarget(index * 10 + offset)),
});
});
runtime.trust.setContacts(Array.from({ length: MUTERS_PER_AUTHOR }, (_, i) => muter(i)));
// 100 trusted muters against each of the first 50 authors.
for (let authorIndex = 0; authorIndex < 50; authorIndex += 1) {
for (let muterIndex = 0; muterIndex < MUTERS_PER_AUTHOR; muterIndex += 1) {
runtime.mutes.replaceList({
owner: muter(muterIndex),
updatedAt: NOW - 1000,
entries: Array.from({ length: 50 }, (_, i) => ({ pubkey: author(i) })),
hasEncryptedEntries: false,
});
}
}
// Reports across several categories.
for (let index = 0; index < 1_000; index += 1) {
runtime.reports.ingest(
{
reporter: muter(index % MUTERS_PER_AUTHOR),
target: eventTarget(index, 0),
category: ["spam", "scam", "misleading"][index % 3],
createdAt: NOW - 500,
},
`event:${eventId(index)}`,
);
}
const targets = Array.from({ length: TARGET_COUNT }, (_, index) =>
eventTarget(index, index % AUTHOR_COUNT),
);
return { runtime, transport, targets, MODERATORS };
}
describe("large feed evaluation", () => {
it("evaluates 5,000 targets without touching the network", () => {
const { runtime, transport, targets } = buildLargeRuntime();
const listSpy = vi.spyOn(transport, "list");
const subscribeSpy = vi.spyOn(transport, "subscribe");
const publishSpy = vi.spyOn(transport, "publish");
const decisions = runtime.evaluateMany(targets, { profile: "feed" });
expect(decisions.size).toBe(TARGET_COUNT);
expect(listSpy).not.toHaveBeenCalled();
expect(subscribeSpy).not.toHaveBeenCalled();
expect(publishSpy).not.toHaveBeenCalled();
});
it("opens a bounded number of subscriptions, not one per card", () => {
const { runtime, transport, targets } = buildLargeRuntime();
const subscribeSpy = vi.spyOn(transport, "subscribe");
runtime.setActiveTargets(targets);
runtime.subscribeToActiveTargetReports();
// 5,000 event targets at 200 per filter chunk.
expect(subscribeSpy).toHaveBeenCalledTimes(Math.ceil(TARGET_COUNT / 200));
});
it("produces the same decisions on a second pass", () => {
const { runtime, targets } = buildLargeRuntime();
const first = runtime.evaluateMany(targets.slice(0, 200), { profile: "feed" });
const second = runtime.evaluateMany(targets.slice(0, 200), { profile: "feed" });
for (const [key, decision] of first) {
expect(second.get(key)).toEqual(decision);
}
});
it("serves a repeated pass from cache", () => {
const { runtime, targets } = buildLargeRuntime();
const slice = targets.slice(0, 500);
runtime.evaluateMany(slice, { profile: "feed" });
const missesAfterFirst = runtime.diagnostics.cacheMisses;
runtime.evaluateMany(slice, { profile: "feed" });
expect(runtime.diagnostics.cacheMisses).toBe(missesAfterFirst);
expect(runtime.diagnostics.cacheHits).toBe(500);
});
it("keeps profiles separate in the cache", () => {
const { runtime, targets } = buildLargeRuntime();
const slice = targets.slice(0, 10);
const feed = runtime.evaluateMany(slice, { profile: "feed" });
const strict = runtime.evaluateMany(slice, { profile: "strict" });
for (const [key, decision] of feed) {
expect(strict.get(key)?.policyProfile).toBe("strict");
expect(decision.policyProfile).toBe("feed");
}
});
it("completes a 5,000-target pass in reasonable time", () => {
const { runtime, targets } = buildLargeRuntime();
const started = performance.now();
runtime.evaluateMany(targets, { profile: "feed" });
// Loose on purpose: this catches an accidental O(n^2), not a slow runner.
expect(performance.now() - started).toBeLessThan(10_000);
});
});
describe("incremental invalidation", () => {
it("updating one report invalidates only that target", () => {
const { runtime, targets } = buildLargeRuntime();
const slice = targets.slice(0, 100);
runtime.evaluateMany(slice, { profile: "feed" });
expect(runtime.decisionCache.size).toBe(100);
const firstKey = `event:${eventId(0)}`;
runtime.reports.ingest(
{ reporter: muter(0), target: slice[0], category: "spam", createdAt: NOW },
firstKey,
);
expect(runtime.decisionCache.size).toBe(99);
expect(runtime.decisionCache.has(`feed|${firstKey}`)).toBe(false);
});
it("updating one author's mute state invalidates only that author's targets", () => {
const { runtime } = buildLargeRuntime();
const userTargets = Array.from({ length: 20 }, (_, index) => userTarget(200 + index));
runtime.evaluateMany(userTargets, { profile: "feed" });
expect(runtime.decisionCache.size).toBe(20);
runtime.mutes.replaceList({
owner: muter(0),
updatedAt: NOW,
entries: [{ pubkey: author(200) }],
hasEncryptedEntries: false,
});
expect(runtime.decisionCache.has(`feed|user:${author(200)}`)).toBe(false);
expect(runtime.decisionCache.has(`feed|user:${author(201)}`)).toBe(true);
});
it("changing policy invalidates every decision", () => {
const { runtime, targets } = buildLargeRuntime();
runtime.evaluateMany(targets.slice(0, 50), { profile: "feed" });
expect(runtime.decisionCache.size).toBe(50);
runtime.policies.setLocalPolicy(
createPolicyDefinition({
id: "perf-v2",
version: "2.0.0",
profiles: { feed: { name: "feed", reports: { default: { hide: 1 } }, mutes: {} } },
}),
);
expect(runtime.decisionCache.size).toBe(0);
});
it("changing the moderator roster invalidates every decision", () => {
const { runtime, targets } = buildLargeRuntime();
runtime.evaluateMany(targets.slice(0, 50), { profile: "feed" });
runtime.admin.setRoles({ root: hex("f", 1), actors: {} });
expect(runtime.decisionCache.size).toBe(0);
});
it("switching viewers invalidates every decision", () => {
const { runtime, targets } = buildLargeRuntime();
runtime.evaluateMany(targets.slice(0, 50), { profile: "feed" });
runtime.setViewer(hex("e", 2));
expect(runtime.decisionCache.size).toBe(0);
});
it("hands out frozen decisions so a consumer cannot corrupt the cache", () => {
const { runtime, targets } = buildLargeRuntime();
const decision = runtime.evaluate(targets[0], { profile: "feed" });
expect(() => {
decision.visibility.effect = "deny";
}).toThrow();
});
});
describe("teardown", () => {
it("leaves no subscriptions or cached decisions", () => {
const { runtime, targets } = buildLargeRuntime();
runtime.setActiveTargets(targets.slice(0, 400));
runtime.subscribeToActiveTargetReports();
runtime.evaluateMany(targets.slice(0, 50), { profile: "feed" });
runtime.destroy();
expect(runtime.subscriptions.size).toBe(0);
expect(runtime.decisionCache.size).toBe(0);
});
it("closes underlying transport subscriptions", () => {
const { runtime, transport, targets } = buildLargeRuntime();
const closes = [];
vi.spyOn(transport, "subscribe").mockImplementation(() => {
const handle = { close: vi.fn() };
closes.push(handle);
return handle;
});
runtime.setActiveTargets(targets.slice(0, 400));
runtime.subscribeToActiveTargetReports();
runtime.destroy();
expect(closes.length).toBeGreaterThan(0);
for (const handle of closes) {
expect(handle.close).toHaveBeenCalled();
}
});
});