mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
refactor(domain): charge resource allowances through one Budget owner (#11293)
This commit is contained in:
1 parent
c931ba43ab
commit
3d67404132
11 files changed
+215
-96
No files matched your search
@@ -500,5 +500,24 @@
|
||||
"src/signals/SigrokRecordingProvider.ts": 1,
|
||||
"src/signals/parseDecoderDiscovery.ts": 1,
|
||||
"src/v8/V8CodeCache.errors.ts": 1
|
||||
},
|
||||
"budget-arithmetic": {
|
||||
"src/application/investigation/EvidenceLedger.ts": 1,
|
||||
"src/artifacts/UnityFsArtifactReader.ts": 1,
|
||||
"src/artifacts/apple/InterfaceBuilderDecodeBudget.ts": 2,
|
||||
"src/artifacts/bun/BunModuleGraphLayout.ts": 2,
|
||||
"src/browser/PlaywrightScenarioEvents.ts": 3,
|
||||
"src/browser/WebBundleAnalysisWorker.ts": 2,
|
||||
"src/browser/execution/CdpRuntimeSources.ts": 2,
|
||||
"src/desktop/DesktopContentStore.ts": 1,
|
||||
"src/domain/analysisView/WebNetworkProjectionAccumulator.ts": 1,
|
||||
"src/domain/apiContract/parseApiContract.ts": 1,
|
||||
"src/domain/firmware/hexRecords.ts": 1,
|
||||
"src/domain/firmware/uf2.ts": 1,
|
||||
"src/domain/javascript/javascriptSemanticWorkAllowance.ts": 1,
|
||||
"src/domain/javascript/javascriptSymbolicBindings.ts": 4,
|
||||
"src/dotnet/ManagedDecodeBudget.ts": 1,
|
||||
"src/javascript/analysis/JavaScriptAnalysisWorker.ts": 4,
|
||||
"src/python/PythonPycParser.ts": 2
|
||||
}
|
||||
}
|
||||
@@ -340,6 +340,19 @@ checkCounted(
|
||||
[],
|
||||
);
|
||||
|
||||
// A resource allowance is charged and refunded through `Budget` in
|
||||
// src/domain/budget, which refuses malformed or oversized charges without
|
||||
// changing the remainder. Hand-written `remaining -=` / `retained +=`
|
||||
// arithmetic repeats that admission and drifts (charge-then-check, no
|
||||
// safe-integer check).
|
||||
checkCounted(
|
||||
"hand-written budget arithmetic (use Budget from src/domain/budget)",
|
||||
"budget-arithmetic",
|
||||
files,
|
||||
/this\.#?\w*(?:[Rr]emaining|[Rr]etained|[Uu]sed)\w*\s*[-+]=/gu,
|
||||
["src/domain/budget.ts"],
|
||||
);
|
||||
|
||||
// A deadline is a monotonic budget owned by `Deadline` in src/domain/deadline;
|
||||
// a wall-clock sum moves with clock changes and has no finalize phase. The
|
||||
// remaining copy converts a monotonic budget for a browser page, which can
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Budget } from "../../domain/budget.js";
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
export class NativeMatchingAdmission {
|
||||
readonly #maximum: number;
|
||||
readonly #source: NativeMatchingAllowance["source"];
|
||||
#remaining: number;
|
||||
readonly #bytes: Budget;
|
||||
#work = 0;
|
||||
readonly #seen = new WeakSet<object>();
|
||||
constructor(
|
||||
@@ -25,29 +26,10 @@ export class NativeMatchingAdmission {
|
||||
// budget this request actually ran under.
|
||||
this.#maximum = allowance.maximumBytes;
|
||||
this.#source = allowance.source;
|
||||
this.#remaining = this.#maximum;
|
||||
this.#bytes = new Budget(this.#maximum);
|
||||
}
|
||||
readonly checkpoint = async (bytes = 0): Promise<void> => {
|
||||
this.#remaining -= bytes;
|
||||
if (this.#remaining < 0)
|
||||
throw new AnalysisResourceConstraintError(
|
||||
this.operation,
|
||||
"capacity",
|
||||
`Native function matching exceeds its ${nativeMatchingByteAllowance(this.#maximum)} representation allowance (native.matching_representation_bytes).`,
|
||||
{ maximum_bytes: this.#maximum },
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction: nativeMatchingAllowanceRecovery({
|
||||
maximumBytes: this.#maximum,
|
||||
source: this.#source,
|
||||
}),
|
||||
limit: analysisSettingLimit(
|
||||
"native.matching_representation_bytes",
|
||||
this.#maximum,
|
||||
this.#source,
|
||||
),
|
||||
},
|
||||
);
|
||||
this.#bytes.charge(bytes, () => this.#exhausted());
|
||||
this.signal?.throwIfAborted();
|
||||
if (++this.#work % 256 === 0)
|
||||
await setImmediate(
|
||||
@@ -55,6 +37,26 @@ export class NativeMatchingAdmission {
|
||||
this.signal === undefined ? {} : { signal: this.signal },
|
||||
);
|
||||
};
|
||||
#exhausted(): AnalysisResourceConstraintError {
|
||||
return new AnalysisResourceConstraintError(
|
||||
this.operation,
|
||||
"capacity",
|
||||
`Native function matching exceeds its ${nativeMatchingByteAllowance(this.#maximum)} representation allowance (native.matching_representation_bytes).`,
|
||||
{ maximum_bytes: this.#maximum },
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction: nativeMatchingAllowanceRecovery({
|
||||
maximumBytes: this.#maximum,
|
||||
source: this.#source,
|
||||
}),
|
||||
limit: analysisSettingLimit(
|
||||
"native.matching_representation_bytes",
|
||||
this.#maximum,
|
||||
this.#source,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Charge a borrowed record's envelope, which matching copies into its
|
||||
* limitations, digests and links. Its result payloads stay resident: the
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Budget } from "../domain/budget.js";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import { readArtifactFileBytes } from "./ArtifactFileRange.js";
|
||||
@@ -39,15 +40,6 @@ const directoryLimit = (): ArtifactReaderFailure =>
|
||||
{ boundary: "memory" },
|
||||
);
|
||||
|
||||
/** Reserve buffers, JS rows and escaped path projections before constructing them. */
|
||||
class UnityFsDirectoryBudget {
|
||||
#remaining = DIRECTORY_BUDGET_BYTES;
|
||||
reserve(bytes: number): void {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining)
|
||||
throw directoryLimit();
|
||||
this.#remaining -= bytes;
|
||||
}
|
||||
}
|
||||
/** Signature, version, two bounded strings and the fixed tail; reads are bounded. */
|
||||
const HEADER_PREFIX_MAX = 8 + 4 + UNITYFS_HEADER_STRING_MAX * 2 + 20;
|
||||
const UTF8 = strictUtf8Decoder("preserve");
|
||||
@@ -258,7 +250,7 @@ export const decodeUnityFsLz4 = async (
|
||||
|
||||
const parseBlocks = async (
|
||||
table: Buffer,
|
||||
budget: UnityFsDirectoryBudget,
|
||||
budget: Budget,
|
||||
signal?: AbortSignal,
|
||||
): Promise<UnityFsBlock[]> => {
|
||||
if (table.length < 24)
|
||||
@@ -267,7 +259,7 @@ const parseBlocks = async (
|
||||
if (20 + blockCount * 10 + 4 > table.length)
|
||||
throw unityFsFormatFailure("declared block count exceeds the table");
|
||||
// Includes array slots, numeric fields and the retained lookup representation.
|
||||
budget.reserve(blockCount * 256);
|
||||
budget.charge(blockCount * 256, directoryLimit);
|
||||
const blocks: UnityFsBlock[] = [];
|
||||
let cursor = 20;
|
||||
let compressedStart = 0;
|
||||
@@ -327,7 +319,7 @@ const parseBlocks = async (
|
||||
const parseNodes = async (
|
||||
table: Buffer,
|
||||
blocks: readonly UnityFsBlock[],
|
||||
budget: UnityFsDirectoryBudget,
|
||||
budget: Budget,
|
||||
signal?: AbortSignal,
|
||||
): Promise<UnityFsNode[]> => {
|
||||
let cursor = 20 + blocks.length * 10;
|
||||
@@ -338,7 +330,7 @@ const parseNodes = async (
|
||||
if (cursor + nodeCount * 21 > table.length)
|
||||
throw unityFsFormatFailure("declared node count exceeds the table");
|
||||
// Covers parsed rows plus the reader's owned entries/map; paths are charged separately.
|
||||
budget.reserve(nodeCount * 512);
|
||||
budget.charge(nodeCount * 512, directoryLimit);
|
||||
const streamSize =
|
||||
blocks.length === 0
|
||||
? 0
|
||||
@@ -357,7 +349,7 @@ const parseNodes = async (
|
||||
const nameEnd = table.indexOf(0, cursor);
|
||||
if (nameEnd === -1) throw unityFsFormatFailure("node path is unterminated");
|
||||
// UTF-16 plus repeated JSON-escaped source/canonical path projections.
|
||||
budget.reserve((nameEnd - cursor) * 24);
|
||||
budget.charge((nameEnd - cursor) * 24, directoryLimit);
|
||||
let path: string;
|
||||
try {
|
||||
path = UTF8.decode(table.subarray(cursor, nameEnd));
|
||||
@@ -428,10 +420,10 @@ export const readUnityFsBlocksInfo = async (
|
||||
header.uncompressedBlocksInfoSize > MAX_TABLE_BYTES
|
||||
)
|
||||
throw directoryLimit();
|
||||
const budget = new UnityFsDirectoryBudget();
|
||||
budget.reserve(header.compressedBlocksInfoSize);
|
||||
const budget = new Budget(DIRECTORY_BUDGET_BYTES);
|
||||
budget.charge(header.compressedBlocksInfoSize, directoryLimit);
|
||||
if (header.compression !== UNITYFS_COMPRESSION_NONE)
|
||||
budget.reserve(header.uncompressedBlocksInfoSize);
|
||||
budget.charge(header.uncompressedBlocksInfoSize, directoryLimit);
|
||||
const compressed = await readArtifactFileBytes(
|
||||
handle,
|
||||
layout.blocksInfoOffset,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Budget } from "../domain/budget.js";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
import {
|
||||
ArtifactReaderFailure,
|
||||
@@ -31,19 +32,13 @@ const UTF8 = strictUtf8Decoder("preserve");
|
||||
export const wxapkgFormatFailure = (detail: string): ArtifactReaderFailure =>
|
||||
new ArtifactReaderFailure("format", `wxapkg is malformed: ${detail}`);
|
||||
|
||||
/** Reserve buffers, JS rows and escaped name projections before allocating. */
|
||||
class WxapkgDirectoryBudget {
|
||||
#remaining = DIRECTORY_BUDGET_BYTES;
|
||||
reserve(bytes: number): void {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining)
|
||||
throw artifactFixedLimitFailure(
|
||||
"wxapkg directory representation",
|
||||
DIRECTORY_BUDGET_BYTES,
|
||||
"bytes",
|
||||
);
|
||||
this.#remaining -= bytes;
|
||||
}
|
||||
}
|
||||
/** Buffers, JS rows and escaped name projections, reserved before allocating. */
|
||||
const directoryLimit = (): Error =>
|
||||
artifactFixedLimitFailure(
|
||||
"wxapkg directory representation",
|
||||
DIRECTORY_BUDGET_BYTES,
|
||||
"bytes",
|
||||
);
|
||||
|
||||
export interface WxapkgHeader {
|
||||
readonly info: number;
|
||||
@@ -138,8 +133,8 @@ export const readWxapkgFileTable = async (
|
||||
MAX_TABLE_BYTES,
|
||||
"bytes",
|
||||
);
|
||||
const budget = new WxapkgDirectoryBudget();
|
||||
budget.reserve(readBytes);
|
||||
const budget = new Budget(DIRECTORY_BUDGET_BYTES);
|
||||
budget.charge(readBytes, directoryLimit);
|
||||
const table = await readArtifactFileBytes(
|
||||
handle,
|
||||
tableStart,
|
||||
@@ -166,7 +161,7 @@ export const readWxapkgFileTable = async (
|
||||
)
|
||||
throw wxapkgFormatFailure("member row exceeds the table");
|
||||
// Row storage plus the reader's owned entries/map; charge before decode.
|
||||
budget.reserve(512 + nameLength * 24);
|
||||
budget.charge(512 + nameLength * 24, directoryLimit);
|
||||
let declaredPath: string;
|
||||
try {
|
||||
declaredPath = UTF8.decode(table.subarray(cursor, cursor + nameLength));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Budget } from "../domain/budget.js";
|
||||
import {
|
||||
noRuntimeOverrideSentence,
|
||||
selectedCdpCommandTimeoutMs,
|
||||
@@ -19,7 +20,7 @@ export const cdpScriptSourceDeadlineOmissionReason = (
|
||||
export class CdpScriptSourceBudget {
|
||||
readonly #deadline: ProviderStartupDeadline;
|
||||
readonly commandTimeoutMs: number;
|
||||
#retainedBytes = 0;
|
||||
readonly #retained = new Budget(WEB_RUNTIME_LIMITS.sourceBytes);
|
||||
|
||||
constructor(
|
||||
signal?: AbortSignal,
|
||||
@@ -39,11 +40,7 @@ export class CdpScriptSourceBudget {
|
||||
|
||||
/** Admit exact UTF-8 source bytes before building or retaining its artifact. */
|
||||
admit(text: string): boolean {
|
||||
const bytes = Buffer.byteLength(text, "utf8");
|
||||
if (bytes > WEB_RUNTIME_LIMITS.sourceBytes - this.#retainedBytes)
|
||||
return false;
|
||||
this.#retainedBytes += bytes;
|
||||
return true;
|
||||
return this.#retained.tryCharge(Buffer.byteLength(text, "utf8"));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Budget } from "../../../domain/budget.js";
|
||||
import { strictUtf8Decoder } from "../../../domain/strictUtf8.js";
|
||||
import { captureBudgets } from "../CaptureBudgets.js";
|
||||
import { readLeb128, ULEB64 } from "../../../domain/leb128.js";
|
||||
@@ -8,19 +9,18 @@ import { CaptureFormatError } from "../CaptureFormatError.js";
|
||||
|
||||
/** One charge covers all selected records, including temporary interpretation strings. */
|
||||
export class ProtobufBudget {
|
||||
#remaining = captureBudgets().outputBytes;
|
||||
readonly #bytes = new Budget(captureBudgets().outputBytes);
|
||||
#work = 0;
|
||||
constructor(readonly signal?: AbortSignal) {}
|
||||
charge(bytes: number): void {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining)
|
||||
throw new CaptureFormatError(
|
||||
{
|
||||
name: "protobuf-decoding",
|
||||
maximum: captureBudgets().outputBytes,
|
||||
},
|
||||
`Protobuf decoding exceeds max_reply_bytes (${String(captureBudgets().outputBytes)} bytes); raise that per-call allowance and retry.`,
|
||||
);
|
||||
this.#remaining -= bytes;
|
||||
this.#bytes.charge(
|
||||
bytes,
|
||||
() =>
|
||||
new CaptureFormatError(
|
||||
{ name: "protobuf-decoding", maximum: this.#bytes.maximum },
|
||||
`Protobuf decoding exceeds max_reply_bytes (${String(this.#bytes.maximum)} bytes); raise that per-call allowance and retry.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
async checkpoint(): Promise<void> {
|
||||
this.signal?.throwIfAborted();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Budget } from "./budget.js";
|
||||
|
||||
describe("Budget", () => {
|
||||
const exhausted = () => new Error("exhausted");
|
||||
|
||||
it("charges until the maximum and refuses without charging", () => {
|
||||
const budget = new Budget(10);
|
||||
budget.charge(6, exhausted);
|
||||
expect(budget.tryCharge(5)).toBe(false);
|
||||
expect(() => {
|
||||
budget.charge(5, exhausted);
|
||||
}).toThrow("exhausted");
|
||||
expect(budget.used).toBe(6);
|
||||
budget.charge(4, exhausted);
|
||||
expect(budget.remaining).toBe(0);
|
||||
});
|
||||
|
||||
it.each([-1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1])(
|
||||
"refuses a malformed charge %s without changing the remainder",
|
||||
(amount) => {
|
||||
const budget = new Budget(Number.MAX_SAFE_INTEGER);
|
||||
expect(budget.tryCharge(amount)).toBe(false);
|
||||
expect(budget.used).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it("refunds without rising above the maximum", () => {
|
||||
const budget = new Budget(10);
|
||||
budget.charge(4, exhausted);
|
||||
budget.refund(3);
|
||||
expect(budget.used).toBe(1);
|
||||
budget.refund(5);
|
||||
expect(budget.remaining).toBe(10);
|
||||
expect(() => {
|
||||
budget.refund(-1);
|
||||
}).toThrow(/refund/u);
|
||||
});
|
||||
|
||||
it("rejects a malformed maximum", () => {
|
||||
expect(() => new Budget(-1)).toThrow(/maximum/u);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { invariantViolation } from "./internalInvariant.js";
|
||||
|
||||
const isAmount = (value: number): boolean =>
|
||||
Number.isSafeInteger(value) && value >= 0;
|
||||
|
||||
/**
|
||||
* One allowance of a counted resource (bytes, records, visits) that a producer
|
||||
* charges before it allocates and refunds when it discards a reservation.
|
||||
*
|
||||
* The budget owns the arithmetic: a charge must be a safe non-negative integer
|
||||
* that fits what remains, a refused charge changes nothing, and a refund never
|
||||
* raises the remainder above the maximum. Each owner keeps its own limit
|
||||
* failure, passed to `charge` as `exhausted`.
|
||||
*/
|
||||
export class Budget {
|
||||
#used = 0;
|
||||
|
||||
constructor(readonly maximum: number) {
|
||||
if (!isAmount(maximum))
|
||||
throw invariantViolation(
|
||||
`Budget maximum must be a non-negative safe integer, got ${String(maximum)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Amount charged and not refunded. */
|
||||
get used(): number {
|
||||
return this.#used;
|
||||
}
|
||||
|
||||
/** Amount still available. */
|
||||
get remaining(): number {
|
||||
return this.maximum - this.#used;
|
||||
}
|
||||
|
||||
/** Charge `amount` when it fits; otherwise charge nothing and return false. */
|
||||
tryCharge(amount: number): boolean {
|
||||
if (!isAmount(amount) || amount > this.remaining) return false;
|
||||
this.#used += amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Charge `amount`, or throw the owner's limit failure without charging. */
|
||||
charge(amount: number, exhausted: () => Error): void {
|
||||
if (!this.tryCharge(amount)) throw exhausted();
|
||||
}
|
||||
|
||||
/** Return a reservation that was not retained. */
|
||||
refund(amount: number): void {
|
||||
if (!isAmount(amount))
|
||||
throw invariantViolation(
|
||||
`Budget refund must be a non-negative safe integer, got ${String(amount)}`,
|
||||
);
|
||||
this.#used = Math.max(0, this.#used - amount);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Budget } from "../budget.js";
|
||||
import { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
|
||||
import {
|
||||
DEFAULT_WASM_INTERFACE_REPRESENTATION_BYTES,
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
|
||||
/** One selected interface allowance, shared by nested producer reads and comparison. */
|
||||
export class WasmInterfaceBudget {
|
||||
#remaining: number;
|
||||
readonly #bytes: Budget;
|
||||
readonly maximumBytes: number;
|
||||
readonly source: "default" | "request";
|
||||
constructor(
|
||||
@@ -16,26 +17,27 @@ export class WasmInterfaceBudget {
|
||||
this.maximumBytes =
|
||||
maximumBytes ?? DEFAULT_WASM_INTERFACE_REPRESENTATION_BYTES;
|
||||
this.source = maximumBytes === undefined ? "default" : "request";
|
||||
this.#remaining = this.maximumBytes;
|
||||
this.#bytes = new Budget(this.maximumBytes);
|
||||
}
|
||||
reserve(bytes: number): void {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining)
|
||||
throw new AnalysisResourceConstraintError(
|
||||
this.operation,
|
||||
"capacity",
|
||||
`Typed WASM interface representation exceeds max_interface_bytes (${String(this.maximumBytes)} bytes)`,
|
||||
{
|
||||
maximum_bytes: this.maximumBytes,
|
||||
default_bytes: DEFAULT_WASM_INTERFACE_REPRESENTATION_BYTES,
|
||||
maximum_supported_bytes: MAX_WASM_INTERFACE_REPRESENTATION_BYTES,
|
||||
source: this.source,
|
||||
unit: "bytes",
|
||||
},
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction: `${this.maximumBytes < MAX_WASM_INTERFACE_REPRESENTATION_BYTES ? `Raise max_interface_bytes above ${String(this.maximumBytes)} bytes (maximum ${String(MAX_WASM_INTERFACE_REPRESENTATION_BYTES)}) on the next ${this.operation} call.` : `max_interface_bytes is already at the runtime representation maximum of ${String(MAX_WASM_INTERFACE_REPRESENTATION_BYTES)} bytes; no higher interface allowance is supported. A comparison may select only imports or exports; otherwise report the unsupported complete representation.`} No restart is required. Other output, heap and deadline controls do not raise this allowance. A comparison cannot restore facts omitted from partial producer Evidence; inspect the original module with a larger producer budget first when that budget is below its maximum.`,
|
||||
},
|
||||
);
|
||||
this.#remaining -= bytes;
|
||||
this.#bytes.charge(bytes, () => this.#exhausted());
|
||||
}
|
||||
#exhausted(): AnalysisResourceConstraintError {
|
||||
return new AnalysisResourceConstraintError(
|
||||
this.operation,
|
||||
"capacity",
|
||||
`Typed WASM interface representation exceeds max_interface_bytes (${String(this.maximumBytes)} bytes)`,
|
||||
{
|
||||
maximum_bytes: this.maximumBytes,
|
||||
default_bytes: DEFAULT_WASM_INTERFACE_REPRESENTATION_BYTES,
|
||||
maximum_supported_bytes: MAX_WASM_INTERFACE_REPRESENTATION_BYTES,
|
||||
source: this.source,
|
||||
unit: "bytes",
|
||||
},
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction: `${this.maximumBytes < MAX_WASM_INTERFACE_REPRESENTATION_BYTES ? `Raise max_interface_bytes above ${String(this.maximumBytes)} bytes (maximum ${String(MAX_WASM_INTERFACE_REPRESENTATION_BYTES)}) on the next ${this.operation} call.` : `max_interface_bytes is already at the runtime representation maximum of ${String(MAX_WASM_INTERFACE_REPRESENTATION_BYTES)} bytes; no higher interface allowance is supported. A comparison may select only imports or exports; otherwise report the unsupported complete representation.`} No restart is required. Other output, heap and deadline controls do not raise this allowance. A comparison cannot restore facts omitted from partial producer Evidence; inspect the original module with a larger producer budget first when that budget is below its maximum.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Budget } from "../domain/budget.js";
|
||||
import {
|
||||
malformedInput,
|
||||
ParseFailureError,
|
||||
@@ -117,13 +118,13 @@ export const modelCapacityReportedLimits = (
|
||||
|
||||
/** Charge representation growth before retaining rows or decoding long strings. */
|
||||
export class ModelReportBudget {
|
||||
#remaining = MODEL_REPORT_BYTES;
|
||||
readonly #bytes = new Budget(MODEL_REPORT_BYTES);
|
||||
charge(bytes: number): void {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining)
|
||||
throw modelCapacityFailure(
|
||||
this.#bytes.charge(bytes, () =>
|
||||
modelCapacityFailure(
|
||||
"representation-bytes",
|
||||
"Model facts exceed the 32 MiB retained representation budget. This release has no runtime override.",
|
||||
);
|
||||
this.#remaining -= bytes;
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user