Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api-reference/veryfront/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ import {
| -------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `requireRateLimitKey` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/middleware/builtin/security/rate-limit-validation.ts#L7) |
| `requireRateLimitWindowMs` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/middleware/builtin/security/rate-limit-validation.ts#L25) |
| `unrefTimer` | Unreference a timer to prevent it from keeping the process alive | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L287) |
| `unrefTimer` | Unreference a timer to prevent it from keeping the process alive | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L314) |

#### Types

Expand Down
1 change: 1 addition & 0 deletions src/platform/compat/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
getRuntimeVersion,
getStdout,
getTerminalSize,
getV8HeapSizeLimit,
isInteractive,
isStdoutTTY,
memoryUsage,
Expand Down
16 changes: 16 additions & 0 deletions src/platform/compat/process/lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import { isBun } from "../runtime.ts";
import { getV8HeapSizeLimit } from "./lifecycle.ts";

describe("platform/compat/process/lifecycle", () => {
it("rejects Bun's moving node:v8 compatibility heap limit", () => {
if (!isBun) return;
assertEquals(
getV8HeapSizeLimit(),
undefined,
"Bun's process-derived node:v8 shim value is not a fixed heap ceiling",
);
});
});
27 changes: 27 additions & 0 deletions src/platform/compat/process/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ export function memoryUsage(): {
return { rss, heapTotal, heapUsed, external: external || 0 };
}

/**
* Read the real V8 heap size limit in bytes from runtime heap statistics
* (`node:v8` `getHeapStatistics().heap_size_limit`).
*
* Unlike env strings such as `DENO_V8_FLAGS`, this reflects the limit V8 is
* actually enforcing. Returns `undefined` when the runtime does not expose a
* fixed V8 heap limit. Bun's `node:v8` compatibility value is process-derived
* and changes as peak memory grows, so it is intentionally not accepted.
*/
export function getV8HeapSizeLimit(): number | undefined {
if (IS_BUN) return undefined;

try {
const proc = runtimeProcess as
| { getBuiltinModule?: (id: string) => unknown }
| null;
const v8 = proc?.getBuiltinModule?.("node:v8") as
| { getHeapStatistics?: () => { heap_size_limit?: number } }
| undefined;
const limit = v8?.getHeapStatistics?.().heap_size_limit;
return typeof limit === "number" && Number.isFinite(limit) && limit > 0 ? limit : undefined;
} catch (_) {
/* expected: runtimes without node:v8 support */
return undefined;
}
}

/**
* Check if stdin is a TTY (terminal)
*/
Expand Down
81 changes: 81 additions & 0 deletions src/utils/memory/profiler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import "#veryfront/schemas/_test-setup.ts";
import { assert, assertEquals } from "#veryfront/testing/assert.ts";
import { afterEach, describe, it } from "#veryfront/testing/bdd.ts";
import { isBun } from "#veryfront/platform/compat/runtime.ts";
import { withEnv } from "#veryfront/testing";
import {
checkMemoryPressure,
DEFAULT_PROFILER_CRITICAL_THRESHOLD,
Expand All @@ -14,6 +16,7 @@ import {
getMemorySnapshot,
getRapidHeapGrowthEvaluation,
registerCache,
resolveEffectiveHeapLimitMB,
setHeapWarningThreshold,
startMemoryMonitoring,
stopMemoryMonitoring,
Expand Down Expand Up @@ -98,6 +101,84 @@ describe("memory/profiler", () => {
});
});

describe("heap limit honesty", () => {
it("reports the runtime heap limit, not the DENO_V8_FLAGS env string", async () => {
if (isBun) return;
const { getHeapStatistics } = await import("node:v8");
const runtimeLimitMB = getHeapStatistics().heap_size_limit / (1024 * 1024);

await withEnv({ DENO_V8_FLAGS: "--max-old-space-size=999999" }, async () => {
const stats = getHeapStats();
assert(
Math.abs(stats.heapSizeLimitMB - runtimeLimitMB) < 1,
`heapSizeLimitMB (${stats.heapSizeLimitMB}) must reflect the real V8 heap_size_limit ` +
`(${runtimeLimitMB.toFixed(2)}MB), not the unverified env string`,
);
});
});

it("clamps an unverified DENO_V8_FLAGS limit to the V8 default ceiling", () => {
const effective = resolveEffectiveHeapLimitMB({
runtimeHeapLimitMB: undefined,
configuredHeapLimitMB: 4096,
});

assertEquals(
effective,
2048,
"an unverified 4096MB env limit must resolve to the 2048MB V8 default",
);
});
Comment thread
kojiwakayama marked this conversation as resolved.

it("uses the conservative fallback instead of Bun's moving node:v8 shim value", async () => {
if (!isBun) return;

await withEnv({ DENO_V8_FLAGS: "--max-old-space-size=999999" }, async () => {
assertEquals(
getHeapStats().heapSizeLimitMB,
2048,
"Bun must not use its process-derived node:v8 compatibility value as a heap ceiling",
);
});
});

it("reports over-threshold pressure at ~1.6GB used when a 4096MB flag is unverified", () => {
const effective = resolveEffectiveHeapLimitMB({
runtimeHeapLimitMB: undefined,
configuredHeapLimitMB: 4096,
});
const heapUsedPercent = (1638.4 / effective) * 100;

assertEquals(
evaluateMemoryPressure(heapUsedPercent, { warning: 65, critical: 75 }),
{ critical: true, warning: true },
"1638.4MB used against the effective limit must exceed a 75% eviction threshold",
);
});

it("uses the runtime-verified limit as-is when heap statistics expose it", () => {
assertEquals(
resolveEffectiveHeapLimitMB({
runtimeHeapLimitMB: 4096,
configuredHeapLimitMB: 4096,
}),
4096,
"a limit confirmed by runtime heap statistics is trusted as-is",
);
});

it("falls back to the V8 default when nothing is configured or verifiable", () => {
assertEquals(
resolveEffectiveHeapLimitMB({
runtimeHeapLimitMB: undefined,
configuredHeapLimitMB: undefined,
}),
2048,
"with no runtime or configured limit the V8 default old-space ceiling applies",
);
});
});

describe("getMemorySnapshot", () => {
it("should return a snapshot with expected properties", () => {
const snapshot = getMemorySnapshot();
Expand Down
67 changes: 61 additions & 6 deletions src/utils/memory/profiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,25 @@
**************************/

import { rendererLogger } from "#veryfront/utils";
import { getArgs, getEnv, memoryUsage } from "#veryfront/platform/compat/process.ts";
import {
getArgs,
getEnv,
getV8HeapSizeLimit,
memoryUsage,
} from "#veryfront/platform/compat/process.ts";

const logger = rendererLogger.component("memory-profiler");

/** Fallback V8 heap limit when no --max-old-space-size flag is set (5 GB) */
const DEFAULT_HEAP_LIMIT_MB = 5_120;
/**
* V8's default old-space ceiling (2 GB on 64-bit) — the honest cap for any
* heap limit that cannot be verified against runtime heap statistics.
*
* The actual default heap_size_limit scales with available system memory
* (e.g. ~4 GB on large machines), so this floor is deliberately
* conservative: if it is ever wrong, pressure eviction fires earlier than
* strictly necessary, never later.
*/
const V8_DEFAULT_HEAP_LIMIT_MB = 2_048;

/** Default interval for periodic memory snapshots (30 seconds) */
export const DEFAULT_MEMORY_MONITORING_INTERVAL_MS = 30_000;
Expand Down Expand Up @@ -131,7 +144,10 @@ export function getHeapStats(): HeapStats {

const usedHeapSizeMB = mem.heapUsed / (1024 * 1024);
const totalHeapSizeMB = mem.heapTotal / (1024 * 1024);
const heapSizeLimitMB = getConfiguredHeapLimit();
const heapSizeLimitMB = resolveEffectiveHeapLimitMB({
runtimeHeapLimitMB: getRuntimeHeapLimitMB(),
configuredHeapLimitMB: getConfiguredHeapLimit(),
});
const externalMemoryMB = mem.external / (1024 * 1024);
const heapUsedPercent = (usedHeapSizeMB / heapSizeLimitMB) * 100;

Expand All @@ -145,7 +161,46 @@ export function getHeapStats(): HeapStats {
};
}

function getConfiguredHeapLimit(): number {
export interface EffectiveHeapLimitInput {
/** Real V8 `heap_size_limit` (MB) read from runtime heap statistics, when available. */
runtimeHeapLimitMB: number | undefined;
/** Limit (MB) parsed from CLI args / `DENO_V8_FLAGS` env strings — unverified. */
configuredHeapLimitMB: number | undefined;
}

/**
* Resolve the heap limit used for `heapUsedPercent` and memory-pressure
* thresholds.
*
* The runtime-reported limit always wins. Env/CLI strings like
* `DENO_V8_FLAGS` only describe a limit the runtime may silently ignore:
* `deno compile` binaries have shipped without applying `DENO_V8_FLAGS`,
* aborting at V8's ~2048MB default while the env string claimed 4096MB —
* which made pressure eviction mathematically unreachable
* (veryfront-issue-inbox#269). When the runtime limit cannot be read, an
* env-derived limit is therefore clamped to the V8 default so eviction
* still fires before the real ceiling.
*/
export function resolveEffectiveHeapLimitMB(input: EffectiveHeapLimitInput): number {
const { runtimeHeapLimitMB, configuredHeapLimitMB } = input;

if (runtimeHeapLimitMB !== undefined && runtimeHeapLimitMB > 0) {
return Math.round(runtimeHeapLimitMB * 100) / 100;
}

if (configuredHeapLimitMB !== undefined && configuredHeapLimitMB > 0) {
return Math.min(configuredHeapLimitMB, V8_DEFAULT_HEAP_LIMIT_MB);
}

return V8_DEFAULT_HEAP_LIMIT_MB;
}

function getRuntimeHeapLimitMB(): number | undefined {
const limitBytes = getV8HeapSizeLimit();
return limitBytes !== undefined ? limitBytes / (1024 * 1024) : undefined;
}

function getConfiguredHeapLimit(): number | undefined {
const args = getArgs().join(" ");

const v8FlagsMatch = args.match(/--max-old-space-size=(\d+)/);
Expand All @@ -158,7 +213,7 @@ function getConfiguredHeapLimit(): number {
const v8MaxOldSpaceSize = parseInt(getEnv("V8_MAX_OLD_SPACE_SIZE") ?? "", 10);
if (!Number.isNaN(v8MaxOldSpaceSize) && v8MaxOldSpaceSize > 0) return v8MaxOldSpaceSize;

return DEFAULT_HEAP_LIMIT_MB;
return undefined;
}

export function getCacheStats(): CacheStats[] {
Expand Down