mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
fix(managed): charge admitted member facts once in the final projection (#3487)
* fix(managed): charge admitted member facts once in the final projection inspect_managed_members admits method bodies, signatures, call edges, field accesses and user strings as they are produced, then admits the whole result again before returning it. The final pass re-counted every already-admitted record, so the 64 MiB representation budget was spent at roughly twice the result's JSON size. Ordinary assemblies such as MathNet.Numerics, MongoDB.Driver, Npgsql, ClosedXML, System.Reactive and Azure.Storage.Blobs returned resource-limit with no members. The budget now remembers each admitted projection. A containing projection spends that charge once instead of counting the subtree again; a value that serializes more than once still pays for every further occurrence. Results that already fit are byte-identical. Refs #3449 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(managed): check a repeated occurrence pays with fresh credit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c9764084b8
commit
bddeae5108
2 files changed
+53
-1
No files matched your search
@@ -35,6 +35,35 @@ describe("managed projection admission", () => {
|
||||
ManagedReaderFailure,
|
||||
);
|
||||
});
|
||||
|
||||
it("charges an admitted record once inside its containing projection", () => {
|
||||
const budget = new ManagedDecodeBudget();
|
||||
const record = admitManagedProjection(
|
||||
{ name: "a".repeat(40 * 1024 * 1024) },
|
||||
budget,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
admitManagedProjection({ records: [record] }, budget),
|
||||
).not.toThrow();
|
||||
// The containing projection spent the credit, so later reuse pays again.
|
||||
expect(() =>
|
||||
admitManagedProjection({ records: [record, record] }, budget),
|
||||
).toThrow(ManagedReaderFailure);
|
||||
});
|
||||
|
||||
it("charges a repeated occurrence of an admitted record again", () => {
|
||||
const budget = new ManagedDecodeBudget();
|
||||
const record = admitManagedProjection(
|
||||
{ name: "a".repeat(40 * 1024 * 1024) },
|
||||
budget,
|
||||
);
|
||||
|
||||
// The first occurrence spends the credit; the second serializes again.
|
||||
expect(() =>
|
||||
admitManagedProjection({ records: [record, record] }, budget),
|
||||
).toThrow(ManagedReaderFailure);
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed decoding bounds", () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ export class ManagedDecodeBudget {
|
||||
readonly #rows = new Map<number, Set<number>>();
|
||||
readonly #strings = new Map<number, CachedString>();
|
||||
readonly #digests = new WeakMap<ArrayBufferLike, Map<string, string>>();
|
||||
readonly #prepaid = new WeakSet<object>();
|
||||
|
||||
reserve(bytes: number, offset: number | null): void {
|
||||
this.#admit(bytes, offset);
|
||||
@@ -82,6 +83,16 @@ export class ManagedDecodeBudget {
|
||||
return digest;
|
||||
}
|
||||
|
||||
/** Record an admitted projection; its next serialized occurrence is already charged. */
|
||||
prepay(value: object): void {
|
||||
this.#prepaid.add(value);
|
||||
}
|
||||
|
||||
/** Spend an admitted projection's charge once; repeated occurrences pay again. */
|
||||
consumePrepaid(value: object): boolean {
|
||||
return this.#prepaid.delete(value);
|
||||
}
|
||||
|
||||
cachedString(index: number): CachedString | undefined {
|
||||
return this.#strings.get(index);
|
||||
}
|
||||
@@ -141,12 +152,23 @@ const jsonBytes = (item: unknown): number =>
|
||||
? managedJsonStringBytes(item)
|
||||
: Buffer.byteLength(JSON.stringify(item) ?? "null");
|
||||
|
||||
/** Admit repeated values too: shared heap strings still expand at the JSON boundary. */
|
||||
/**
|
||||
* Admit repeated values too: shared heap strings still expand at the JSON
|
||||
* boundary. An object admitted earlier was charged for one occurrence, so a
|
||||
* containing projection spends that charge instead of counting it again.
|
||||
* Admitted values must stay unmodified.
|
||||
*/
|
||||
export const admitManagedProjection = <Value>(
|
||||
value: Value,
|
||||
budget: ManagedDecodeBudget = new ManagedDecodeBudget(),
|
||||
): Value => {
|
||||
const visit = (item: unknown): void => {
|
||||
if (
|
||||
item !== null &&
|
||||
typeof item === "object" &&
|
||||
budget.consumePrepaid(item)
|
||||
)
|
||||
return;
|
||||
if (Array.isArray(item)) {
|
||||
budget.reserve(item.length + 2, null);
|
||||
for (const child of item) visit(child);
|
||||
@@ -161,5 +183,6 @@ export const admitManagedProjection = <Value>(
|
||||
}
|
||||
};
|
||||
visit(value);
|
||||
if (value !== null && typeof value === "object") budget.prepay(value);
|
||||
return value;
|
||||
};
|
||||
Reference in new issue
Block a user