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:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-14 17:19:33 -07:00
1 parent c9764084b8
commit bddeae5108
2 files changed
+53 -1

No files matched your search

+29
View File
@@ -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", () => {
+24 -1
View File
@@ -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;
};