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.
This commit is contained in:
thePR0M3TH3AN committed 2026-08-09 18:07:23 -04:00
1 parent 2beb7a7bb5
commit c4477ae84b
8 files changed
+1091 -35

No files matched your search

+155 -22
View File
@@ -6,7 +6,6 @@
import {
NEUTRAL_POLICY,
evaluateMany,
evaluateTarget,
getTargetKey,
isValidTarget,
@@ -26,6 +25,8 @@ import {
selectReplaceable,
} from "@nostr-governance/nostr";
import { snapshotFingerprint } from "@nostr-governance/core";
import { Emitter } from "./emitter.js";
import { createMemoryStorage, createNullSigner, storageKey } from "./interfaces.js";
import {
@@ -75,6 +76,22 @@ export function chunk(items, size) {
return chunks;
}
/**
* Recursively freeze a decision so cached instances cannot be mutated.
* @template T
* @param {T} value
* @returns {T}
*/
function deepFreeze(value) {
if (value && typeof value === "object" && !Object.isFrozen(value)) {
Object.freeze(value);
for (const entry of Object.values(value)) {
deepFreeze(entry);
}
}
return value;
}
export class GovernanceRuntime extends Emitter {
/**
* @param {Object} options
@@ -136,16 +153,57 @@ export class GovernanceRuntime extends Emitter {
/** @type {Array<() => void>} */
this.storeUnsubscribes = [
this.admin.on("change", () => this.emit("change", { source: "admin" })),
this.trust.on("change", () => this.emit("change", { source: "trust" })),
this.reports.on("change", (detail) => this.emit("change", { source: "reports", ...detail })),
this.mutes.on("change", () => this.emit("change", { source: "mutes" })),
this.policies.on("change", () => this.emit("change", { source: "policy" })),
this.overrides.on("change", () => this.emit("change", { source: "overrides" })),
// Administrative, trust, and policy changes can alter any decision, so
// they drop the whole cache. Report and mute changes name the targets
// they touch, so only those entries are invalidated.
this.admin.on("change", () => {
this.invalidateDecisions();
this.emit("change", { source: "admin" });
}),
this.trust.on("change", () => {
this.invalidateDecisions();
this.emit("change", { source: "trust" });
}),
this.reports.on("change", (detail) => {
this.invalidateDecisions(detail?.targetKey ? [detail.targetKey] : undefined);
this.emit("change", { source: "reports", ...detail });
}),
this.mutes.on("change", (detail) => {
this.invalidateDecisions(detail?.targetKeys);
this.emit("change", { source: "mutes" });
}),
this.policies.on("change", () => {
this.invalidateDecisions();
this.emit("change", { source: "policy" });
}),
this.overrides.on("change", (detail) => {
this.invalidateDecisions(detail?.targetKey ? [detail.targetKey] : undefined);
this.emit("change", { source: "overrides" });
}),
];
/** @type {{ eventsIngested: number, unknownEvents: number, subscriptions: number }} */
this.diagnostics = { eventsIngested: 0, unknownEvents: 0, subscriptions: 0 };
// Decisions are cached per (profile, target). A feed re-renders far more
// often than governance state changes, and re-deriving an unchanged verdict
// for every card is the cost the extraction is supposed to remove.
/** @type {Map<string, GovernanceDecision>} */
this.decisionCache = new Map();
// The snapshot and its fingerprint are rebuilt only when a store changes.
// Deriving them per target walks every report and mute list, which turns a
// large feed into quadratic work.
/** @type {GovernanceSnapshot|null} */
this.cachedSnapshot = null;
/** @type {string} */
this.cachedSnapshotFingerprint = "";
/** @type {{ eventsIngested: number, unknownEvents: number, subscriptions: number, cacheHits: number, cacheMisses: number }} */
this.diagnostics = {
eventsIngested: 0,
unknownEvents: 0,
subscriptions: 0,
cacheHits: 0,
cacheMisses: 0,
};
}
/**
@@ -340,6 +398,7 @@ export class GovernanceRuntime extends Emitter {
return;
}
this.viewerPubkey = normalized;
this.decisionCache.clear();
this.trust.clearViewerState();
this.overrides.clearOverrides();
this.emit("viewer", { pubkey: normalized });
@@ -347,16 +406,44 @@ export class GovernanceRuntime extends Emitter {
/**
* Build a snapshot for the pure evaluator.
*
* Memoized until a store reports a change, so repeated evaluation over one
* feed reuses a single materialized view of state.
*
* @returns {GovernanceSnapshot}
*/
snapshot() {
return {
if (this.cachedSnapshot) {
return this.cachedSnapshot;
}
this.cachedSnapshot = {
authority: this.admin.authority,
admin: this.admin.state,
trust: { contacts: this.trust.contacts, seeds: this.trust.seeds },
reports: this.reports.toRecordMap(),
trustedMutes: this.mutes.toRecordMap(),
};
return this.cachedSnapshot;
}
/**
* The current snapshot fingerprint, computed once per snapshot.
* @returns {string}
*/
snapshotFingerprint() {
if (!this.cachedSnapshotFingerprint) {
const snapshot = this.snapshot();
const policy = this.policies.policy;
this.cachedSnapshotFingerprint = snapshotFingerprint({
authority: snapshot.authority,
admin: snapshot.admin,
reports: snapshot.reports,
trustedMutes: snapshot.trustedMutes,
policy: { id: policy.id, version: policy.version },
});
}
return this.cachedSnapshotFingerprint;
}
/** @returns {ViewerState} */
@@ -368,6 +455,34 @@ export class GovernanceRuntime extends Emitter {
};
}
/**
* Drop cached decisions.
*
* With no argument every entry is dropped; with target keys, only decisions
* for those targets across all profiles.
*
* @param {string[]} [targetKeys]
*/
invalidateDecisions(targetKeys) {
// Any change that reaches a decision also invalidates the materialized
// snapshot it was derived from.
this.cachedSnapshot = null;
this.cachedSnapshotFingerprint = "";
if (!targetKeys) {
this.decisionCache.clear();
return;
}
const affected = new Set(targetKeys);
for (const cacheKey of Array.from(this.decisionCache.keys())) {
// Cache keys are `${profile}|${targetKey}`.
const targetKey = cacheKey.slice(cacheKey.indexOf("|") + 1);
if (affected.has(targetKey)) {
this.decisionCache.delete(cacheKey);
}
}
}
/**
* Evaluate one target.
* @param {GovernanceTarget} target
@@ -377,7 +492,14 @@ export class GovernanceRuntime extends Emitter {
* @returns {GovernanceDecision}
*/
evaluate(target, { profile, surface } = {}) {
return evaluateTarget(
const cacheKey = `${profile ?? ""}|${getTargetKey(target)}`;
const cached = this.decisionCache.get(cacheKey);
if (cached) {
this.diagnostics.cacheHits += 1;
return cached;
}
const decision = evaluateTarget(
target,
this.snapshot(),
{
@@ -385,9 +507,16 @@ export class GovernanceRuntime extends Emitter {
policyProfile: profile,
policy: this.policies.policy,
now: this.now(),
snapshotFingerprint: this.snapshotFingerprint(),
},
this.viewerState(),
);
this.diagnostics.cacheMisses += 1;
// Frozen because the cache hands out the same object to every caller; a
// consumer mutating a decision would otherwise corrupt later reads.
this.decisionCache.set(cacheKey, deepFreeze(decision));
return decision;
}
/**
@@ -399,17 +528,18 @@ export class GovernanceRuntime extends Emitter {
* @returns {Map<string, GovernanceDecision>}
*/
evaluateMany(targets, { profile, surface } = {}) {
return evaluateMany(
targets,
this.snapshot(),
{
surface: surface ?? profile ?? "default",
policyProfile: profile,
policy: this.policies.policy,
now: this.now(),
},
this.viewerState(),
);
/** @type {Map<string, GovernanceDecision>} */
const results = new Map();
for (const target of targets ?? []) {
if (!isValidTarget(target)) {
continue;
}
const key = getTargetKey(target);
if (!results.has(key)) {
results.set(key, this.evaluate(target, { profile, surface }));
}
}
return results;
}
/** Diagnostic summary for support and debugging. */
@@ -449,6 +579,9 @@ export class GovernanceRuntime extends Emitter {
unsubscribe();
}
this.decisionCache.clear();
this.cachedSnapshot = null;
this.cachedSnapshotFingerprint = "";
this.admin.clear();
this.trust.clear();
this.reports.clear();
+19 -4
View File
@@ -328,12 +328,20 @@ export class TrustedMuteStore extends Emitter {
if (existing && existing.updatedAt > list.updatedAt) {
return;
}
const affected = new Set();
for (const entry of existing?.entries ?? []) {
affected.add(`user:${entry.pubkey}`);
}
for (const entry of list.entries) {
affected.add(`user:${entry.pubkey}`);
}
this.lists.set(list.owner, {
owner: list.owner,
updatedAt: list.updatedAt,
entries: list.entries,
});
this.#emitChange();
this.#emitChange(Array.from(affected));
}
/**
@@ -345,15 +353,21 @@ export class TrustedMuteStore extends Emitter {
return 0;
}
const cutoff = this.now() - this.windowSeconds;
const affected = new Set();
let pruned = 0;
for (const [owner, list] of this.lists.entries()) {
if (list.updatedAt < cutoff) {
for (const entry of list.entries) {
affected.add(`user:${entry.pubkey}`);
}
this.lists.delete(owner);
pruned += 1;
}
}
if (pruned) {
this.#emitChange();
this.#emitChange(Array.from(affected));
}
return pruned;
}
@@ -389,13 +403,14 @@ export class TrustedMuteStore extends Emitter {
return counts;
}
#emitChange() {
/** @param {string[]} [targetKeys] - Targets whose decisions this change affects */
#emitChange(targetKeys) {
this.fingerprint = fingerprint(
Array.from(this.lists.values())
.map((list) => [list.owner, list.updatedAt, list.entries.length])
.sort(),
);
this.emit("change", { fingerprint: this.fingerprint });
this.emit("change", { fingerprint: this.fingerprint, targetKeys });
}
}