perf(dotnet): index declaring-type ranges once per metadata table (#918)
perf(dotnet): index ordered declaring-type ranges once per table
This commit is contained in:
1 parent
900d7c8606
commit
7a9f587c3a
3 files changed
+137
-3
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import { fc, it } from "@fast-check/vitest";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import {
|
||||
createDeclaringTypeLookup,
|
||||
declaringType,
|
||||
type TypeRange,
|
||||
} from "./ManagedMemberInspectorCore.js";
|
||||
|
||||
const range = (
|
||||
name: string,
|
||||
fieldStart: number,
|
||||
fieldEnd: number,
|
||||
methodStart = fieldStart,
|
||||
methodEnd = fieldEnd,
|
||||
): TypeRange => ({
|
||||
token: name,
|
||||
fullName: name,
|
||||
fieldStart,
|
||||
fieldEnd,
|
||||
methodStart,
|
||||
methodEnd,
|
||||
});
|
||||
|
||||
describe("managed declaring type ownership", () => {
|
||||
it("resolves field and method boundaries independently, including empty types and gaps", () => {
|
||||
const ranges = [
|
||||
range("first", 1, 3, 1, 1),
|
||||
range("empty", 3, 3, 1, 1),
|
||||
range("last", 5, 7, 1, 4),
|
||||
];
|
||||
const field = createDeclaringTypeLookup(ranges, "field");
|
||||
const method = createDeclaringTypeLookup(ranges, "method");
|
||||
expect([0, 1, 2, 3, 4, 5, 6, 7].map(field)).toEqual([
|
||||
null,
|
||||
{ token: "first", fullName: "first" },
|
||||
{ token: "first", fullName: "first" },
|
||||
null,
|
||||
null,
|
||||
{ token: "last", fullName: "last" },
|
||||
{ token: "last", fullName: "last" },
|
||||
null,
|
||||
]);
|
||||
expect([0, 1, 2, 3, 4].map(method)).toEqual([
|
||||
null,
|
||||
{ token: "last", fullName: "last" },
|
||||
{ token: "last", fullName: "last" },
|
||||
{ token: "last", fullName: "last" },
|
||||
null,
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains the first observed owner for overlapping, reversed, or out-of-order ranges", () => {
|
||||
const ranges = [
|
||||
range("first", 5, 9),
|
||||
range("overlap", 1, 7),
|
||||
range("reversed", 10, 2),
|
||||
];
|
||||
const lookup = createDeclaringTypeLookup(ranges, "method");
|
||||
expect(lookup(6)).toEqual({ token: "first", fullName: "first" });
|
||||
expect(lookup(2)).toEqual({ token: "overlap", fullName: "overlap" });
|
||||
expect(lookup(10)).toBeNull();
|
||||
expect(createDeclaringTypeLookup([], "method")(1)).toBeNull();
|
||||
});
|
||||
|
||||
it.prop([
|
||||
fc.array(
|
||||
fc.record({
|
||||
fieldStart: fc.integer({ min: 0, max: 30 }),
|
||||
fieldEnd: fc.integer({ min: 0, max: 30 }),
|
||||
methodStart: fc.integer({ min: 0, max: 30 }),
|
||||
methodEnd: fc.integer({ min: 0, max: 30 }),
|
||||
}),
|
||||
{ maxLength: 20 },
|
||||
),
|
||||
])(
|
||||
"agrees with observed first-match ownership for arbitrary metadata ranges",
|
||||
(raw) => {
|
||||
const ranges = raw.map((value, index) => ({
|
||||
...value,
|
||||
token: String(index),
|
||||
fullName: `Type${String(index)}`,
|
||||
}));
|
||||
for (const table of ["field", "method"] as const) {
|
||||
const lookup = createDeclaringTypeLookup(ranges, table);
|
||||
for (const row of [
|
||||
NaN,
|
||||
-Infinity,
|
||||
Infinity,
|
||||
...Array.from({ length: 32 }, (_, i) => i),
|
||||
])
|
||||
expect(lookup(row)).toEqual(declaringType(ranges, table, row));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -150,6 +150,42 @@ export const declaringType = (
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Resolve member ownership without rescanning ordered type ranges per row. */
|
||||
export const createDeclaringTypeLookup = (
|
||||
ranges: readonly TypeRange[],
|
||||
table: "field" | "method",
|
||||
): ((row: number) => ReturnType<typeof declaringType>) => {
|
||||
const entries = ranges.map((range) => ({
|
||||
range,
|
||||
start: table === "field" ? range.fieldStart : range.methodStart,
|
||||
end: table === "field" ? range.fieldEnd : range.methodEnd,
|
||||
}));
|
||||
const ordered = entries.every((entry, index) => {
|
||||
const previous = entries[index - 1];
|
||||
return (
|
||||
entry.start <= entry.end &&
|
||||
(previous === undefined || previous.end <= entry.start)
|
||||
);
|
||||
});
|
||||
// Malformed metadata can overlap or reverse ranges. Retain the original
|
||||
// first-match interpretation rather than guessing a different owner.
|
||||
if (!ordered) return (row) => declaringType(ranges, table, row);
|
||||
return (row) => {
|
||||
let first = 0;
|
||||
let end = entries.length;
|
||||
while (first < end) {
|
||||
const middle = first + Math.floor((end - first) / 2);
|
||||
const entry = entries[middle];
|
||||
if (entry === undefined) return null;
|
||||
if (row >= entry.start && row < entry.end)
|
||||
return { token: entry.range.token, fullName: entry.range.fullName };
|
||||
if (row < entry.start) end = middle;
|
||||
else first = middle + 1;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
export const parseTypes = (
|
||||
bytes: Buffer,
|
||||
layout: ManagedMetadataLayout,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ManagedPeLayout } from "./ManagedPeReader.js";
|
||||
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
declaringType,
|
||||
createDeclaringTypeLookup,
|
||||
signature,
|
||||
type FieldCore,
|
||||
type ManagedCallEdge,
|
||||
@@ -33,6 +33,7 @@ export const parseFields = (
|
||||
const fields: ManagedField[] = [];
|
||||
const core = new Map<string, FieldCore>();
|
||||
const table = layout.table(4);
|
||||
const declaringType = createDeclaringTypeLookup(ranges, "field");
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = metadataRowCursor(bytes, layout, 4, row);
|
||||
const flags = cursor.readUInt16();
|
||||
@@ -48,7 +49,7 @@ export const parseFields = (
|
||||
cursor.readIndex(layout.blobIndexSize),
|
||||
layout.blob.size,
|
||||
);
|
||||
const declared = declaringType(ranges, "field", row);
|
||||
const declared = declaringType(row);
|
||||
const token = metadataToken(4, row);
|
||||
fields.push({
|
||||
token,
|
||||
@@ -123,6 +124,7 @@ export const parseMethods = ({
|
||||
const methods: ManagedMethod[] = [];
|
||||
const core = new Map<string, MethodCore>();
|
||||
const table = layout.table(6);
|
||||
const declaringType = createDeclaringTypeLookup(ranges, "method");
|
||||
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
|
||||
const cursor = metadataRowCursor(bytes, layout, 6, row);
|
||||
const rva = cursor.readUInt32();
|
||||
@@ -141,7 +143,7 @@ export const parseMethods = ({
|
||||
layout.blob.size,
|
||||
);
|
||||
cursor.readIndex(layout.tableIndexSize(8));
|
||||
const declared = declaringType(ranges, "method", row);
|
||||
const declared = declaringType(row);
|
||||
const token = metadataToken(6, row);
|
||||
methods.push({
|
||||
token,
|
||||
|
||||
Reference in new issue
Block a user