diff --git a/packages/data-model/src/index.ts b/packages/data-model/src/index.ts --- a/packages/data-model/src/index.ts +++ b/packages/data-model/src/index.ts @@ -38,6 +38,11 @@ toStructuredDebugValue, } from "./value-debug.ts"; -export { hashOf, hashStringOf, taggedHashStringOf } from "./value-hash.ts"; +export { + getFrozenObjectHashCacheHits, + hashOf, + hashStringOf, + taggedHashStringOf, +} from "./value-hash.ts"; export * from "@/types"; diff --git a/packages/data-model/src/value-hash.ts b/packages/data-model/src/value-hash.ts --- a/packages/data-model/src/value-hash.ts +++ b/packages/data-model/src/value-hash.ts @@ -485,6 +485,13 @@ */ const frozenObjectHashCache = new WeakMap(); +let frozenObjectHashCacheHits = 0; + +/** Counts `hashOf` and `hashStringOf` calls served by the frozen-object cache. */ +export function getFrozenObjectHashCacheHits(): number { + return frozenObjectHashCacheHits; +} + /** * Returns an already computed immutable hash without reading the value. * @@ -568,6 +575,7 @@ // checks needed on the fast path. const cached = frozenObjectHashCache.get(obj); if (cached !== undefined) { + frozenObjectHashCacheHits += 1; return cached; } diff --git a/packages/runner/src/storage/extended-storage-transaction.ts b/packages/runner/src/storage/extended-storage-transaction.ts --- a/packages/runner/src/storage/extended-storage-transaction.ts +++ b/packages/runner/src/storage/extended-storage-transaction.ts @@ -211,6 +211,9 @@ onPreparedTx?(): void; + /** Whether the prepared digest was computed or reused at the activity epoch. */ + onPreparedDigest?(outcome: "computed" | "memo"): void; + /** One dereference trace was recorded, and how many the transaction holds * after it. `probeBelongsToDereference` scans this set once per read * activity at commit preparation, so its size is a per-read multiplier. @@ -624,9 +627,16 @@ /** * The activity epoch (see * `IExtendedStorageTransaction.probeFlowLabelWork()`), which counts every - * journaled read, write, dereference trace, and trigger read. + * journaled read, write, and change to prepared-digest decision inputs. */ #cfcActivityEpoch = 0; + + /** Digest and input bound to one immutable activity snapshot. */ + #preparedDigestMemo: { + epoch: number; + input: PreparedDigestInput; + digest: string; + } | undefined; /** * The last _negative_ flow-label probe verdict, with the activity epoch it @@ -646,19 +656,20 @@ } /** - * The prepared-digest input this transaction would hand to verification, - * which a test reads directly to pin what it carries. + * The prepared-digest input and epoch-bound computation, which tests and + * benchmarks drive directly to check binding and cache reuse. */ get accessForTestingOnly(): { buildPreparedDigestInput(): PreparedDigestInput; + preparedDigest(): string; } { return { buildPreparedDigestInput: () => this.#buildPreparedDigestInput(), + preparedDigest: () => this.#preparedDigest(), }; } - /** Stage C tuning T1: any transaction activity that could change the - * flow-label probe's answer moves the epoch. */ + /** Retires activity-bound flow-label and prepared-digest memos. */ #noteCfcActivity(): void { this.#cfcActivityEpoch += 1; } @@ -1074,6 +1085,7 @@ */ setCfcPolicySnapshot(snapshot: PolicySnapshot | undefined): void { if (this.#cfcPolicySnapshotPinned) return; + this.#noteCfcActivity(); this.#cfcPolicySnapshotPinned = true; this.#cfcState.policySnapshot = snapshot === undefined ? undefined @@ -1109,6 +1121,7 @@ >, ): void { if (this.#cfcModuleDelegationsPinned) return; + this.#noteCfcActivity(); this.#cfcModuleDelegationsPinned = true; const snapshot = new Map< MemorySpace, @@ -1408,6 +1421,7 @@ } invalidateCfc(reason: string): void { + this.#preparedDigestMemo = undefined; const wasPrepared = this.#cfcState.prepare.status === "prepared"; const previousDigest = this.#cfcState.prepare.status === "prepared" ? this.#cfcState.prepare.digest @@ -1676,17 +1690,18 @@ #hasWrites = false; /** - * Record that this transaction has written. + * Record that a storage write succeeded. * - * Called from every write path rather than inferred from one of their side + * Called after each successful write rather than inferred from one of its side * effects: a mergeable op and a folded SQLite write are both writes, and * neither drops the read-result cache — the value write a mergeable op * annotates has already done that, and a SQLite op changes no cell value * locally. Deriving "has written" from cache invalidation would miss both. + * Activity is recorded separately because a failed attempt can enter the + * journal without changing a value. */ #noteWrite(): void { this.#hasWrites = true; - this.#noteCfcActivity(); } #invalidateReadResultCache(): void { @@ -1745,7 +1760,8 @@ } setCfcTrustSnapshot(snapshot: TrustSnapshot | undefined): void { - this.#cfcState.trustSnapshot = snapshot; + this.#noteCfcActivity(); + this.#cfcState.trustSnapshot = deepFreeze(snapshot); if (this.#cfcState.prepare.status === "prepared") { this.invalidateCfc("trust-snapshot-changed"); } @@ -1754,7 +1770,8 @@ setCfcImplementationIdentity( identity: ImplementationIdentity | undefined, ): void { - this.#cfcState.implementationIdentity = identity; + this.#noteCfcActivity(); + this.#cfcState.implementationIdentity = deepFreeze(identity); if (this.#cfcState.prepare.status === "prepared") { this.invalidateCfc("implementation-identity-changed"); } @@ -1769,6 +1786,7 @@ // existing WeakMap. The within-sort tiebreaker in // `compareWritePolicyInput` then re-hashes each element via the cache. const frozen = deepFreeze(input); + this.#noteCfcActivity(); this.#cfcState.writePolicyInputs.push(frozen); if (frozen.kind === "schema") { let documents = this.#schemaPolicyInputs.get(frozen.target.space); @@ -1931,12 +1949,14 @@ if (this.#cfcState.consultedGrants[index].digest === consulted.digest) { return; } + this.#noteCfcActivity(); this.#cfcState.consultedGrants[index] = deepFreeze(consulted); if (this.#cfcState.prepare.status === "prepared") { this.invalidateCfc("consulted-grant-changed"); } return; } + this.#noteCfcActivity(); this.#cfcState.consultedGrants.push(deepFreeze(consulted)); // Grants are consulted DURING prepare (the boundary gates); recording // after a prepare stamped its digest means the decision inputs grew — @@ -1958,12 +1978,14 @@ ) { return; } + this.#noteCfcActivity(); this.#cfcState.consultedPolicyManifests[index] = deepFreeze(consulted); if (this.#cfcState.prepare.status === "prepared") { this.invalidateCfc("consulted-policy-manifest-changed"); } return; } + this.#noteCfcActivity(); this.#cfcState.consultedPolicyManifests.push(deepFreeze(consulted)); if (this.#cfcState.prepare.status === "prepared") { this.invalidateCfc("consulted-policy-manifest-added"); @@ -1981,6 +2003,7 @@ if (observation.confidentiality.length === 0) { return; } + this.#noteCfcActivity(); this.#cfcState.labelMetadataObservations.push(deepFreeze(observation)); // A labeled metadata observation makes the transaction CFC-relevant // directly (like noteSystemWrite): its taint must reach the flow @@ -2134,6 +2157,18 @@ commitVerdict(): Promise> { return this.#verdict.promise; + } + + #preparedDigest(): string { + if (this.#preparedDigestMemo?.epoch === this.#cfcActivityEpoch) { + this.#cfcInstrumentation.onPreparedDigest?.("memo"); + return this.#preparedDigestMemo.digest; + } + const input = this.#buildPreparedDigestInput(); + const digest = preparedDigestFor(input); + this.#preparedDigestMemo = { epoch: this.#cfcActivityEpoch, input, digest }; + this.#cfcInstrumentation.onPreparedDigest?.("computed"); + return digest; } #buildPreparedDigestInput(): PreparedDigestInput { @@ -2724,8 +2759,8 @@ this.#cfcState.diagnostics.push(...reasons.map(plainReason)); return ""; } - const preparedInput = this.#buildPreparedDigestInput(); - const digest = preparedDigestFor(preparedInput); + const digest = this.#preparedDigest(); + const preparedInput = this.#preparedDigestMemo!.input; this.#cfcState.prepare = { status: "prepared", digest, @@ -2843,8 +2878,10 @@ // later reshape or mixed-op leave a stale tail op in the commit — silent // corruption. When poison is unavailable the intent is simply not recorded, // so the commit falls back to the plain whole-array diff already written. - if (this.tx.poisonMergeableOp) { - this.tx.recordMergeableOp?.(address, delta); + if (this.tx.poisonMergeableOp && this.tx.recordMergeableOp) { + this.#noteCfcActivity(); + this.tx.recordMergeableOp(address, delta); + this.#noteWrite(); } } @@ -2862,7 +2899,9 @@ "storage transaction does not support recordSqliteWrite()", ); } + this.#noteCfcActivity(); this.tx.recordSqliteWrite(space, op); + this.#noteWrite(); } getReadActivities(): Iterable { @@ -2968,6 +3007,7 @@ options?: IWriteOptions, ): Result { this.#assertWritable("write()"); + this.#noteCfcActivity(); this.#noteSystemWrite(address, value, options); this.#noteWriteIdentity(); if (this.#cfcState.prepare.status === "prepared") { @@ -2979,6 +3019,7 @@ } const result = this.tx.write(address, value, options); if (result.ok) { + this.#noteWrite(); this.#stageSchemaDocsForValue(address.space, address, value); } return result; @@ -2990,6 +3031,7 @@ options?: IWriteOptions, ): void { this.#assertWritable("writeOrThrow()"); + this.#noteCfcActivity(); this.#noteSystemWrite(address, value, options); this.#noteWriteIdentity(); if (this.#cfcState.prepare.status === "prepared") { @@ -3070,6 +3112,7 @@ } else if (writeResult.error) { throw toThrowable(writeResult.error); } + this.#noteWrite(); // The staged value may carry link schemas — or be, or carry, a // `schema` metadata member — with external refs; stage their closure // with it (the write-side delivery guarantee, and what makes a @@ -3127,7 +3170,10 @@ // Capture the identity per yielded write, not once up front: an empty // batch authored nothing, so it must not record a write for the // transaction's write-identity summary. - const noteWriteIdentity = () => this.#noteWriteIdentity(); + const noteWriteIdentity = () => { + this.#noteCfcActivity(); + this.#noteWriteIdentity(); + }; const refuseMalformedSchemaMeta = ( address: IMemorySpaceAddress, value: FabricValue | undefined, @@ -3172,6 +3218,7 @@ if (result.error) { throw toThrowable(result.error); } + if (cachesInvalidated) this.#noteWrite(); for (const write of staged) { this.#stageSchemaDocsForValue( write.address.space, @@ -3196,6 +3243,7 @@ this.#statusOverride = undefined; this.#clearPostCommitOutbox(); this.#cfcState.prepare = { status: "unprepared" }; + this.#preparedDigestMemo = undefined; this.#cfcState.dereferenceTraces = []; this.#cfcState.structureContainers = []; const result = this.tx.abort(reason); @@ -3409,9 +3457,7 @@ } if (this.#cfcState.prepare.status === "prepared") { - const currentDigest = preparedDigestFor( - this.#buildPreparedDigestInput(), - ); + const currentDigest = this.#preparedDigest(); if (currentDigest !== this.#cfcState.prepare.digest) { this.invalidateCfc("prepared-digest-mismatch"); if (this.#cfcState.enforcementMode !== "observe") {