fix(managed): bind row budget charges to metadata identities (#1897)
Charge fixed row bookkeeping once per table and row within an admitted metadata layout. Repeated owner lookups no longer permanently consume capacity for temporary cursors; distinct rows and failed admissions retain the existing representation limit and typed failure behavior. Verify repeated local attribute-constructor lookups through public CLI metadata/member inspection and preserve the remaining capacity guards. Closes #1878 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
5 files changed
+142
-25
No files matched your search
@@ -38,6 +38,18 @@ describe("managed projection admission", () => {
|
||||
});
|
||||
|
||||
describe("managed decoding bounds", () => {
|
||||
it("bounds new row identities without charging lookup replay or merging tables", () => {
|
||||
const budget = new ManagedDecodeBudget();
|
||||
budget.reserve(64 * 1024 * 1024 - 512, null);
|
||||
budget.reserveRow(2, 1, 100);
|
||||
budget.reserveRow(12, 1, 200);
|
||||
|
||||
expect(() => budget.reserveRow(2, 1, 100)).not.toThrow();
|
||||
expect(() => budget.reserveRow(2, 2, 300)).toThrow(ManagedReaderFailure);
|
||||
// A rejected row has no retained reservation; replay cannot bypass capacity.
|
||||
expect(() => budget.reserveRow(2, 2, 300)).toThrow(ManagedReaderFailure);
|
||||
});
|
||||
|
||||
it("keeps concurrent decoding bounds within one budget until released", () => {
|
||||
const budget = new ManagedDecodeBudget();
|
||||
const bound = 40 * 1024 * 1024;
|
||||
|
||||
@@ -15,6 +15,7 @@ interface CachedString {
|
||||
export class ManagedDecodeBudget {
|
||||
#remaining = MAX_METADATA_REPRESENTATION_BYTES;
|
||||
#transient = 0;
|
||||
readonly #rows = new Map<number, Set<number>>();
|
||||
readonly #strings = new Map<number, CachedString>();
|
||||
readonly #digests = new WeakMap<ArrayBufferLike, Map<string, string>>();
|
||||
|
||||
@@ -23,6 +24,18 @@ export class ManagedDecodeBudget {
|
||||
this.#remaining -= bytes;
|
||||
}
|
||||
|
||||
/** Retain one bookkeeping reservation per physical row, independently of lookup replay. */
|
||||
reserveRow(table: number, row: number, offset: number): void {
|
||||
let rows = this.#rows.get(table);
|
||||
if (rows?.has(row) === true) return;
|
||||
this.reserve(256, offset);
|
||||
if (rows === undefined) {
|
||||
rows = new Set<number>();
|
||||
this.#rows.set(table, rows);
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
/** Bound a decoding allocation that stays live until `releaseTransient`; only retained output is reserved. */
|
||||
admitTransient(bytes: number, offset: number | null): void {
|
||||
this.#admit(bytes, offset);
|
||||
|
||||
@@ -245,8 +245,9 @@ export const metadataRowCursor = (
|
||||
"Metadata table is absent",
|
||||
start,
|
||||
);
|
||||
// Reserve per-row object/index bookkeeping before readers retain decoded facts.
|
||||
managedDecodeBudget(layout).reserve(256, start);
|
||||
// Temporary cursors may revisit a row while resolving owners or references.
|
||||
// Bind fixed retained bookkeeping to that row's identity, not each lookup.
|
||||
managedDecodeBudget(layout).reserveRow(table, row, start);
|
||||
return new MetadataRowCursor(
|
||||
bytes,
|
||||
start,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { createHash } from "node:crypto";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
|
||||
interface ManagedPeFixtureOptions {
|
||||
readonly additionalEmptyTypes?: number;
|
||||
readonly customAttributeRowCount?: number;
|
||||
readonly cliFlags?: number;
|
||||
readonly corruptMetadataSignature?: boolean;
|
||||
readonly customAttributeTypeRaw?: number;
|
||||
@@ -20,6 +22,7 @@ interface ManagedPeFixtureOptions {
|
||||
readonly malformedCustomAttributeRows?: readonly number[];
|
||||
readonly metadataValidMaskExtra?: bigint;
|
||||
readonly methodName?: string;
|
||||
readonly methodFlags?: number;
|
||||
readonly methodSignature?: Buffer;
|
||||
readonly moduleRowCount?: number;
|
||||
readonly assemblyRowCount?: number;
|
||||
@@ -328,22 +331,30 @@ const assemblyRefRow = (
|
||||
metadataIndex(0, blobSize),
|
||||
]);
|
||||
|
||||
const customAttributeRow = (
|
||||
value: number,
|
||||
options: ManagedPeFixtureOptions,
|
||||
parentIndexSize: 2 | 4,
|
||||
typeIndexSize: MetadataIndexSize,
|
||||
blobSize: MetadataIndexSize,
|
||||
): Buffer =>
|
||||
const customAttributeRow = ({
|
||||
value,
|
||||
options,
|
||||
parentIndexSize,
|
||||
typeIndexSize,
|
||||
blobSize,
|
||||
row,
|
||||
}: {
|
||||
readonly value: number;
|
||||
readonly options: ManagedPeFixtureOptions;
|
||||
readonly parentIndexSize: 2 | 4;
|
||||
readonly typeIndexSize: MetadataIndexSize;
|
||||
readonly blobSize: MetadataIndexSize;
|
||||
readonly row: number;
|
||||
}): Buffer =>
|
||||
Buffer.concat([
|
||||
parentIndexSize === 2
|
||||
? u16(
|
||||
options.malformedCustomAttributeRows?.includes(1)
|
||||
options.malformedCustomAttributeRows?.includes(row)
|
||||
? 0xffff
|
||||
: (1 << 5) | 14,
|
||||
)
|
||||
: u32(
|
||||
options.malformedCustomAttributeRows?.includes(1)
|
||||
options.malformedCustomAttributeRows?.includes(row)
|
||||
? 0xffff_ffff
|
||||
: (1 << 5) | 14,
|
||||
),
|
||||
@@ -484,6 +495,7 @@ interface FixtureMetadataRowContext {
|
||||
readonly attributeNamespace: number;
|
||||
readonly constructorName: number;
|
||||
readonly typeName: number;
|
||||
readonly additionalTypeNames: readonly number[];
|
||||
readonly typeNamespace: number;
|
||||
readonly fieldName: number;
|
||||
readonly methodName: number;
|
||||
@@ -536,15 +548,16 @@ const customAttributeRows = (
|
||||
): readonly Buffer[] =>
|
||||
options.targetFramework === null
|
||||
? []
|
||||
: [
|
||||
customAttributeRow(
|
||||
indexes.attributeBlob,
|
||||
: Array.from({ length: options.customAttributeRowCount ?? 1 }, (_, index) =>
|
||||
customAttributeRow({
|
||||
value: indexes.attributeBlob,
|
||||
options,
|
||||
widths.hasCustomAttribute,
|
||||
widths.customAttributeType,
|
||||
heapSizes.blobs,
|
||||
),
|
||||
];
|
||||
parentIndexSize: widths.hasCustomAttribute,
|
||||
typeIndexSize: widths.customAttributeType,
|
||||
blobSize: heapSizes.blobs,
|
||||
row: index + 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const exportedTypeTable = (
|
||||
options: ManagedPeFixtureOptions,
|
||||
@@ -577,12 +590,17 @@ const fixtureIndexWidths = ({
|
||||
const rowCounts = new Map<number, number>([
|
||||
[0, options.moduleRowCount ?? 1],
|
||||
[1, 1],
|
||||
[2, 1],
|
||||
[2, indexes.additionalTypeNames.length + 1],
|
||||
[4, 1],
|
||||
[6, 1],
|
||||
[8, 0],
|
||||
[10, 1],
|
||||
[12, options.targetFramework === null ? 0 : 1],
|
||||
[
|
||||
12,
|
||||
options.targetFramework === null
|
||||
? 0
|
||||
: (options.customAttributeRowCount ?? 1),
|
||||
],
|
||||
...(indexes.pinvokeModuleName === null ? [] : ([[26, 1]] as const)),
|
||||
...(indexes.pinvokeImportName === null ? [] : ([[28, 1]] as const)),
|
||||
[32, options.assemblyRowCount ?? 1],
|
||||
@@ -648,9 +666,9 @@ const fixtureMetadataRows = ({
|
||||
],
|
||||
[
|
||||
2,
|
||||
[
|
||||
[...indexes.additionalTypeNames, indexes.typeName].map((name) =>
|
||||
typeDefRow({
|
||||
name: indexes.typeName,
|
||||
name,
|
||||
namespace: indexes.typeNamespace,
|
||||
fieldList: 1,
|
||||
methodList: 1,
|
||||
@@ -662,7 +680,7 @@ const fixtureMetadataRows = ({
|
||||
? {}
|
||||
: { extendsRaw: options.extendsRaw }),
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
[
|
||||
4,
|
||||
@@ -685,7 +703,9 @@ const fixtureMetadataRows = ({
|
||||
blobSize: heapSizes.blobs,
|
||||
parameterSize: widths.table(8),
|
||||
rva: methodRva,
|
||||
flags: options.pinvoke === undefined ? 0x0016 : 0x2016,
|
||||
flags:
|
||||
options.methodFlags ??
|
||||
(options.pinvoke === undefined ? 0x0016 : 0x2016),
|
||||
}),
|
||||
],
|
||||
],
|
||||
@@ -774,6 +794,10 @@ const buildManagedFixtureMetadata = (
|
||||
const attributeNamespace = strings.add("System.Runtime.Versioning");
|
||||
const constructorName = strings.add(".ctor");
|
||||
const typeName = strings.add(options.typeName ?? "Program");
|
||||
const additionalTypeNames = Array.from(
|
||||
{ length: options.additionalEmptyTypes ?? 0 },
|
||||
(_, index) => strings.add(`Empty${String(index)}`),
|
||||
);
|
||||
const typeNamespace = strings.add(options.typeNamespace ?? "Fixture");
|
||||
const fieldName = strings.add(options.fieldName ?? "counter");
|
||||
const methodName = strings.add(options.methodName ?? "Main");
|
||||
@@ -825,6 +849,7 @@ const buildManagedFixtureMetadata = (
|
||||
attributeNamespace,
|
||||
constructorName,
|
||||
typeName,
|
||||
additionalTypeNames,
|
||||
typeNamespace,
|
||||
fieldName,
|
||||
methodName,
|
||||
|
||||
@@ -7,6 +7,72 @@ import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"managed CLI retains metadata and CIL after repeated local constructor owner lookups",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-managed-row-lookup-");
|
||||
const path = join(root, "local-attributes.dll");
|
||||
await writeFile(
|
||||
path,
|
||||
buildManagedPeFixture({
|
||||
additionalEmptyTypes: 512,
|
||||
customAttributeRowCount: 512,
|
||||
customAttributeTypeRaw: (1 << 3) | 2,
|
||||
typeName: "LookupAttribute",
|
||||
extendsRaw: 0,
|
||||
methodName: ".ctor",
|
||||
methodFlags: 0x1886,
|
||||
methodSignature: Buffer.from([0x20, 0x01, 0x01, 0x0e]),
|
||||
ilBody: Buffer.from([0x06, 0x2a]),
|
||||
}),
|
||||
);
|
||||
const artifact = await cli.run({
|
||||
arguments: ["inspect-managed-artifact", path, "--json"],
|
||||
});
|
||||
expect(artifact.exitCode).toBe(0);
|
||||
expect(artifact.json).toMatchObject({
|
||||
normalized_result: {
|
||||
coverage: { state: "complete", issues: [] },
|
||||
attributes: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type_name: "Fixture.LookupAttribute",
|
||||
constructor_token: "0x06000001",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(artifact.json).toHaveProperty(
|
||||
"normalized_result.attributes.length",
|
||||
512,
|
||||
);
|
||||
const members = await cli.run({
|
||||
arguments: ["inspect-managed-members", path, "--json"],
|
||||
});
|
||||
expect(members.exitCode).toBe(0);
|
||||
expect(members.json).toMatchObject({
|
||||
normalized_result: {
|
||||
coverage: { state: "complete", issues: [] },
|
||||
types: expect.arrayContaining([
|
||||
expect.objectContaining({ full_name: "Fixture.Empty0" }),
|
||||
expect.objectContaining({ full_name: "Fixture.Empty511" }),
|
||||
expect.objectContaining({ full_name: "Fixture.LookupAttribute" }),
|
||||
]),
|
||||
methods: [
|
||||
expect.objectContaining({
|
||||
name: ".ctor",
|
||||
declaring_type: "Fixture.LookupAttribute",
|
||||
body: expect.objectContaining({
|
||||
status: "present",
|
||||
instruction_count: 1,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(members.json).toHaveProperty("normalized_result.types.length", 513);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"managed CLI separates .NET Standard API targets from runtime evidence",
|
||||
async ({ cli }) => {
|
||||
|
||||
Reference in new issue
Block a user