mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
8413ec08f7
* fix(process): report the effective regex worker heap Node gives --max-old-space-size, including NODE_OPTIONS, precedence over the worker's 128 MiB resourceLimits request. Name that effective old-generation heap and stop saying the request has no runtime override. The matching deadline stays a fixed cap. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): keep SIGXCPU and SIGXFSZ retries uncertain The signal does not prove the retained rlimit was exhausted, so an unchanged retry may succeed. Project that retry as possible instead of saying it fails the same way. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): leave percentage heap overrides unestablished --max-old-space-size-percentage takes precedence over the absolute flag in either order. Node computes that MiB from startup memory, which a later read does not recover, so the effective heap stays unknown instead of the 128 MiB request or an absolute flag. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): read launch NODE_OPTIONS, not process.env A preload can rewrite process.env.NODE_OPTIONS after V8 has applied the original flags, and those flags are not copied into execArgv. Use the initial environment block when it can be read, and leave the effective heap unestablished when that provenance cannot be recovered. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): leave env-file heap overrides unestablished --env-file and --env-file-if-exists can supply NODE_OPTIONS when that variable was absent from the initial environment. The file is not in /proc/self/environ, so a missing entry is not the 128 MiB request. An initial NODE_OPTIONS is still parsed, because Node does not let the file replace it. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): leave --max-heap-size old generation unestablished A positive --max-heap-size or --max_heap_size sets the total heap, not the old generation, and can stay below the 128 MiB worker request when an old-space flag is also present. The effective old generation stays unestablished while that value is in effect. Zero or empty clears it. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): leave config-file heap options unestablished --experimental-config-file and --experimental-default-config-file can apply a heap percentage from a file that is not launch provenance. The effective old generation stays unestablished when either flag is present, including beside an absolute --max-old-space-size. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): read launch exec argv from the command line process.execArgv can be cleared after V8 applies --max-old-space-size. Linux recovers those tokens from /proc/self/cmdline and leaves the heap unestablished when that vector cannot be separated from the program entry. Heap probes compare pinned absolute limits instead of a percentage of host memory. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(process): leave a title-overwritten command line unestablished process.title and --title replace /proc/self/cmdline with the title. A buffer without a program entry is not an empty option list, so the heap stays unknown instead of the 128 MiB request. The command-line probe does not read /proc off Linux. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
494 lines
16 KiB
TypeScript
494 lines
16 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import {
|
|
execArgvFromCommandLine,
|
|
readLaunchExecArgv,
|
|
readLaunchNodeOptions,
|
|
regexMatchingLimitation,
|
|
regexWorkerHeapError,
|
|
resolveLaunchRegexWorkerHeap,
|
|
resolveRegexWorkerHeap,
|
|
type RegexWorkerHeapLimit,
|
|
} from "./RegexMatcher.js";
|
|
|
|
const effective = (mib: number, overridden: boolean): RegexWorkerHeapLimit => ({
|
|
kind: "effective",
|
|
mib,
|
|
overridden,
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "uses the 128 MiB request when no flag is set",
|
|
execArgv: [],
|
|
nodeOptions: undefined,
|
|
heap: effective(128, false),
|
|
},
|
|
{
|
|
name: "lets a smaller execArgv flag override the request",
|
|
execArgv: ["--max-old-space-size=64"],
|
|
nodeOptions: undefined,
|
|
heap: effective(64, true),
|
|
},
|
|
{
|
|
name: "lets a larger execArgv flag override the request",
|
|
execArgv: ["--require", "./preload.js", "--max_old_space_size=256"],
|
|
nodeOptions: undefined,
|
|
heap: effective(256, true),
|
|
},
|
|
{
|
|
name: "reads a positive NODE_OPTIONS flag that is absent from execArgv",
|
|
execArgv: [],
|
|
nodeOptions: "--no-warnings --max-old-space-size=192",
|
|
heap: effective(192, true),
|
|
},
|
|
{
|
|
name: "lets the last execArgv flag win over NODE_OPTIONS",
|
|
execArgv: ["--max-old-space-size=256"],
|
|
nodeOptions: "--max-old-space-size=96",
|
|
heap: effective(256, true),
|
|
},
|
|
{
|
|
name: "treats a final zero flag as leaving the 128 MiB request in effect",
|
|
execArgv: ["--max-old-space-size=0"],
|
|
nodeOptions: "--max-old-space-size=256",
|
|
heap: effective(128, false),
|
|
},
|
|
{
|
|
name: "accepts quoted NODE_OPTIONS and a leading plus or zeros",
|
|
execArgv: ["--max-old-space-size=+0256"],
|
|
nodeOptions: '"--max-old-space-size=144"',
|
|
heap: effective(256, true),
|
|
},
|
|
{
|
|
name: "uses the last positive flag",
|
|
execArgv: ["--max-old-space-size=256", "--max-old-space-size=96"],
|
|
nodeOptions: undefined,
|
|
heap: effective(96, true),
|
|
},
|
|
{
|
|
name: "leaves the effective heap unknown for an unparsed flag",
|
|
execArgv: ["--max-old-space-size=256.5"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "keeps a later positive flag after an unparsed one",
|
|
execArgv: ["--max-old-space-size=256.5", "--max-old-space-size=80"],
|
|
nodeOptions: undefined,
|
|
heap: effective(80, true),
|
|
},
|
|
{
|
|
name: "leaves an untokenizable NODE_OPTIONS value unknown until execArgv decides",
|
|
execArgv: [],
|
|
nodeOptions: '"--max-old-space-size=256',
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "lets execArgv decide when NODE_OPTIONS cannot be tokenized",
|
|
execArgv: ["--max-old-space-size=80"],
|
|
nodeOptions: '"--max-old-space-size=256',
|
|
heap: effective(80, true),
|
|
},
|
|
{
|
|
name: "leaves a percentage flag unestablished instead of the 128 MiB request",
|
|
execArgv: ["--max-old-space-size-percentage=50"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "lets a percentage flag win over a later absolute flag",
|
|
execArgv: ["--max-old-space-size-percentage=50", "--max-old-space-size=64"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "lets a percentage flag win over an earlier absolute flag",
|
|
execArgv: ["--max-old-space-size=64", "--max-old-space-size-percentage=50"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "lets a NODE_OPTIONS percentage win over an execArgv absolute flag",
|
|
execArgv: ["--max-old-space-size=64"],
|
|
nodeOptions: "--max-old-space-size-percentage=50",
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "lets an execArgv percentage win over a NODE_OPTIONS absolute flag",
|
|
execArgv: ["--max-old-space-size-percentage=50"],
|
|
nodeOptions: "--max-old-space-size=64",
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "recognizes an underscore percentage flag",
|
|
execArgv: ["--max_old_space_size_percentage=50"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "recognizes a percentage flag split from its value",
|
|
execArgv: [
|
|
"--max-old-space-size=64",
|
|
"--max-old-space-size-percentage",
|
|
"50",
|
|
],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not let a later absolute flag replace an invalid percentage",
|
|
execArgv: ["--max-old-space-size-percentage=0", "--max-old-space-size=64"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "leaves a fractional percentage unestablished",
|
|
execArgv: ["--max-old-space-size-percentage=50.5"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not treat a final zero absolute flag as 128 MiB after a percentage",
|
|
execArgv: ["--max-old-space-size-percentage=50", "--max-old-space-size=0"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "leaves an env-file launch unestablished when NODE_OPTIONS was absent",
|
|
execArgv: ["--env-file=rea.env"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "leaves an env-file-if-exists launch unestablished when NODE_OPTIONS was absent",
|
|
execArgv: ["--env-file-if-exists=.env"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not let an absolute flag hide NODE_OPTIONS supplied by an env file",
|
|
execArgv: ["--env-file=rea.env", "--max-old-space-size=64"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "recognizes an env-file flag split from its path",
|
|
execArgv: ["--env-file", "rea.env"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "keeps an inherited NODE_OPTIONS value when an env file cannot override it",
|
|
execArgv: ["--env-file=rea.env"],
|
|
nodeOptions: "--max-old-space-size=64",
|
|
heap: effective(64, true),
|
|
},
|
|
{
|
|
name: "lets a later absolute flag win when an inherited NODE_OPTIONS blocks the env file",
|
|
execArgv: ["--max-old-space-size=32", "--env-file-if-exists=rea.env"],
|
|
nodeOptions: "--max-old-space-size=64",
|
|
heap: effective(32, true),
|
|
},
|
|
{
|
|
name: "keeps the 128 MiB request when an empty NODE_OPTIONS blocks the env file",
|
|
execArgv: ["--env-file=rea.env"],
|
|
nodeOptions: "",
|
|
heap: effective(128, false),
|
|
},
|
|
{
|
|
name: "leaves a config-file percentage source unestablished",
|
|
execArgv: ["--experimental-config-file=node.config.json"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "leaves a default config-file launch unestablished",
|
|
execArgv: ["--experimental-default-config-file"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "recognizes a config-file flag split from its path",
|
|
execArgv: ["--experimental-config-file", "node.config.json"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not let an absolute flag hide a config-file heap option",
|
|
execArgv: [
|
|
"--experimental-config-file=node.config.json",
|
|
"--max-old-space-size=64",
|
|
],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not let a recovered NODE_OPTIONS hide a config-file heap option",
|
|
execArgv: ["--experimental-default-config-file"],
|
|
nodeOptions: "--max-old-space-size=64",
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "leaves a config-file flag in recovered NODE_OPTIONS unestablished",
|
|
execArgv: ["--max-old-space-size=256"],
|
|
nodeOptions: "--experimental-config-file=node.config.json",
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "leaves a positive --max-heap-size unestablished instead of the 128 MiB request",
|
|
execArgv: ["--max-heap-size=64"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "recognizes an underscore --max-heap-size",
|
|
execArgv: ["--max_heap_size=+064"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "leaves a NODE_OPTIONS --max-heap-size unestablished",
|
|
execArgv: [],
|
|
nodeOptions: "--max-heap-size=64",
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not let an old-space flag hide a positive --max-heap-size",
|
|
execArgv: ["--max-heap-size=64", "--max-old-space-size=256"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not let a later --max-heap-size hide an old-space flag",
|
|
execArgv: ["--max-old-space-size=64"],
|
|
nodeOptions: "--max_heap_size=256",
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "lets a final zero --max-heap-size clear the total-heap cap",
|
|
execArgv: ["--max-heap-size=64", "--max-heap-size=0"],
|
|
nodeOptions: undefined,
|
|
heap: effective(128, false),
|
|
},
|
|
{
|
|
name: "keeps an old-space flag when a final --max-heap-size is zero",
|
|
execArgv: ["--max-old-space-size=256", "--max-heap-size="],
|
|
nodeOptions: undefined,
|
|
heap: effective(256, true),
|
|
},
|
|
{
|
|
name: "leaves an unparsed --max-heap-size unestablished",
|
|
execArgv: ["--max-heap-size=64.5", "--max-old-space-size=80"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
{
|
|
name: "does not let a later zero clear an unparsed --max-heap-size",
|
|
execArgv: ["--max-heap-size=64.5", "--max-heap-size=0"],
|
|
nodeOptions: undefined,
|
|
heap: { kind: "unknown" } as const,
|
|
},
|
|
] as const)(
|
|
"resolveRegexWorkerHeap $name",
|
|
({ execArgv, nodeOptions, heap }) => {
|
|
expect(resolveRegexWorkerHeap(execArgv, nodeOptions)).toEqual(heap);
|
|
},
|
|
);
|
|
|
|
it("does not treat a rewritten NODE_OPTIONS value as the launch heap", () => {
|
|
const previous = process.env.NODE_OPTIONS;
|
|
const rewritten = "--max-old-space-size=61-rewritten";
|
|
process.env.NODE_OPTIONS = rewritten;
|
|
try {
|
|
const launch = readLaunchNodeOptions();
|
|
if (process.platform === "linux") {
|
|
expect(launch.kind).toBe("recovered");
|
|
if (launch.kind === "recovered")
|
|
expect(launch.value ?? "").not.toContain("61-rewritten");
|
|
} else {
|
|
expect(launch).toEqual({ kind: "unrecoverable" });
|
|
}
|
|
expect(
|
|
resolveLaunchRegexWorkerHeap(
|
|
{ kind: "recovered", value: ["--max-old-space-size=256"] },
|
|
{ kind: "unrecoverable" },
|
|
),
|
|
).toEqual({ kind: "unknown" });
|
|
expect(
|
|
resolveLaunchRegexWorkerHeap(
|
|
{ kind: "recovered", value: [] },
|
|
{ kind: "recovered", value: "--max_heap_size=64" },
|
|
),
|
|
).toEqual({ kind: "unknown" });
|
|
expect(
|
|
resolveLaunchRegexWorkerHeap(
|
|
{ kind: "unrecoverable" },
|
|
{ kind: "recovered", value: "--max-old-space-size=64" },
|
|
),
|
|
).toEqual({ kind: "unknown" });
|
|
if (launch.kind === "recovered") {
|
|
expect(launch.value).not.toBe(rewritten);
|
|
expect(
|
|
resolveLaunchRegexWorkerHeap(
|
|
{ kind: "recovered", value: ["--max-old-space-size=32"] },
|
|
launch,
|
|
),
|
|
).toEqual(
|
|
resolveRegexWorkerHeap(["--max-old-space-size=32"], launch.value),
|
|
);
|
|
}
|
|
} finally {
|
|
if (previous === undefined) delete process.env.NODE_OPTIONS;
|
|
else process.env.NODE_OPTIONS = previous;
|
|
}
|
|
});
|
|
|
|
const commandLine = (...tokens: readonly string[]): Buffer =>
|
|
Buffer.from(`${tokens.join("\0")}\0`);
|
|
|
|
it("recovers launch exec argv from the command line, not process.execArgv", () => {
|
|
expect(
|
|
execArgvFromCommandLine(
|
|
commandLine(
|
|
"node",
|
|
"--max-old-space-size=64",
|
|
"script.js",
|
|
"--max-old-space-size=256",
|
|
),
|
|
),
|
|
).toEqual({ kind: "recovered", value: ["--max-old-space-size=64"] });
|
|
expect(
|
|
resolveLaunchRegexWorkerHeap(
|
|
execArgvFromCommandLine(
|
|
commandLine("node", "--max-old-space-size=64", "script.js"),
|
|
),
|
|
{ kind: "recovered", value: undefined },
|
|
),
|
|
).toEqual({ kind: "effective", mib: 64, overridden: true });
|
|
expect(resolveRegexWorkerHeap([], undefined)).toEqual({
|
|
kind: "effective",
|
|
mib: 128,
|
|
overridden: false,
|
|
});
|
|
expect(
|
|
execArgvFromCommandLine(
|
|
commandLine(
|
|
"node",
|
|
"--require",
|
|
"preload.cjs",
|
|
"--max_old_space_size=64",
|
|
"script.js",
|
|
),
|
|
),
|
|
).toEqual({
|
|
kind: "recovered",
|
|
value: ["--require", "preload.cjs", "--max_old_space_size=64"],
|
|
});
|
|
expect(
|
|
execArgvFromCommandLine(
|
|
commandLine("node", "-e", "code", "--max-old-space-size=80"),
|
|
),
|
|
).toEqual({
|
|
kind: "recovered",
|
|
value: ["-e", "code", "--max-old-space-size=80"],
|
|
});
|
|
expect(
|
|
execArgvFromCommandLine(
|
|
commandLine("node", "--", "--max-old-space-size=64", "script.js"),
|
|
),
|
|
).toEqual({ kind: "recovered", value: [] });
|
|
expect(
|
|
execArgvFromCommandLine(
|
|
commandLine("node", "--not-a-real-node-option", "script.js"),
|
|
),
|
|
).toEqual({ kind: "unrecoverable" });
|
|
expect(execArgvFromCommandLine(Buffer.alloc(0))).toEqual({
|
|
kind: "unrecoverable",
|
|
});
|
|
expect(execArgvFromCommandLine(Buffer.from([0xff, 0]))).toEqual({
|
|
kind: "unrecoverable",
|
|
});
|
|
expect(execArgvFromCommandLine(commandLine("rea"))).toEqual({
|
|
kind: "unrecoverable",
|
|
});
|
|
expect(
|
|
execArgvFromCommandLine(
|
|
Buffer.concat([Buffer.from("rea\0"), Buffer.alloc(32)]),
|
|
),
|
|
).toEqual({ kind: "unrecoverable" });
|
|
expect(
|
|
resolveLaunchRegexWorkerHeap(
|
|
execArgvFromCommandLine(
|
|
Buffer.concat([Buffer.from("rea\0"), Buffer.alloc(32)]),
|
|
),
|
|
{ kind: "recovered", value: undefined },
|
|
),
|
|
).toEqual({ kind: "unknown" });
|
|
expect(execArgvFromCommandLine(commandLine("node", "script.js"))).toEqual({
|
|
kind: "recovered",
|
|
value: [],
|
|
});
|
|
const before = readLaunchExecArgv();
|
|
const snapshot = [...process.execArgv];
|
|
process.execArgv.length = 0;
|
|
try {
|
|
expect(process.execArgv).toEqual([]);
|
|
expect(readLaunchExecArgv()).toEqual(before);
|
|
if (before.kind === "recovered") expect(before.value).toEqual(snapshot);
|
|
else expect(before).toEqual({ kind: "unrecoverable" });
|
|
} finally {
|
|
process.execArgv.push(...snapshot);
|
|
}
|
|
});
|
|
|
|
it("describes the effective heap and keeps the deadline fixed", () => {
|
|
for (const heap of [
|
|
effective(128, false),
|
|
effective(256, true),
|
|
{ kind: "unknown" } as const,
|
|
]) {
|
|
const limitation = regexMatchingLimitation(heap);
|
|
expect(limitation).toContain(
|
|
"This release has no runtime override for the deadline.",
|
|
);
|
|
expect(limitation).not.toContain("either cap");
|
|
expect(limitation).not.toContain("no runtime override for the heap");
|
|
const projected = projectAnalysisError(
|
|
regexWorkerHeapError("search_procedures", new Error("heap"), heap),
|
|
);
|
|
expect(projected.remediation.action).not.toContain("no runtime override");
|
|
expect(projected.remediation.action).toContain("--max-old-space-size");
|
|
expect(projected.remediation.action).toContain(
|
|
"--max-old-space-size-percentage",
|
|
);
|
|
if (heap.kind === "unknown") {
|
|
expect(limitation).toContain("was not established");
|
|
expect(limitation).toContain("--max-old-space-size-percentage");
|
|
expect(limitation).toContain("--max-heap-size");
|
|
expect(limitation).toContain("--experimental-config-file");
|
|
expect(projected.remediation.action).toContain("--max-heap-size");
|
|
expect(projected.remediation.action).toContain(
|
|
"--experimental-default-config-file",
|
|
);
|
|
expect(projected.message).toContain("was not established");
|
|
expect(projected.details).toMatchObject({
|
|
reported_limits: { requested_worker_heap_mib: 128 },
|
|
});
|
|
expect(projected.details?.reported_limits).toEqual({
|
|
requested_worker_heap_mib: 128,
|
|
});
|
|
} else {
|
|
expect(limitation).toContain(`effective ${String(heap.mib)} MiB`);
|
|
expect(projected.message).toContain(`effective ${String(heap.mib)} MiB`);
|
|
expect(projected.details).toMatchObject({
|
|
reported_limits: {
|
|
worker_heap_mib: heap.mib,
|
|
requested_worker_heap_mib: 128,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
});
|