Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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#L317) |

#### Types

Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/veryfront/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const configPath = resolve(cwd(), "veryfront.config.ts");
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `basename` | Return the last path segment. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/path/basic-operations.ts#L43) |
| `createFileSystem` | Create the runtime-native filesystem implementation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/fs.ts#L449) |
| `cwd` | Return the current working directory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L26) |
| `cwd` | Return the current working directory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L31) |
| `dirname` | Return the parent directory path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/path/basic-operations.ts#L36) |
| `exists` | Return false for a missing path and propagate every other filesystem error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/fs.ts#L522) |
| `extname` | Return the file extension for a path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/path/basic-operations.ts#L51) |
Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference/veryfront/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("math", () => {
| `beforeEach` | Register a hook before each BDD test. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/testing/bdd.ts#L725) |
| `chmod` | Change file permissions, rejecting operational failures. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/fs.ts#L599) |
| `createFileSystem` | Create the runtime-native filesystem implementation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/fs.ts#L449) |
| `cwd` | Return the current working directory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L26) |
| `cwd` | Return the current working directory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L31) |
| `deepEquals` | ********************* Shared utility functions for cross-runtime testing. ********************* | [source](https://github.com/veryfront/veryfront-code/blob/main/src/testing/utils.ts#L5) |
| `delay` | Wait for a duration in milliseconds. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/testing/deno-compat.ts#L123) |
| `deleteEnv` | Delete a process environment variable. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/env.ts#L227) |
Expand All @@ -66,7 +66,7 @@ describe("math", () => {
| `exists` | Return false for a missing path and propagate every other filesystem error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/fs.ts#L522) |
| `exit` | Exit the current process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/testing/deno-compat.ts#L128) |
| `fail` | Fail the current assertion immediately. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/testing/assert.ts#L336) |
| `getArgs` | Get command-line arguments (cross-runtime: Deno.args or process.argv). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L10) |
| `getArgs` | Get command-line arguments (cross-runtime: Deno.args or process.argv). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/lifecycle.ts#L15) |
| `getEnv` | Read an environment variable from the active project scope. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/process/env.ts#L127) |
| `getTestTimeScale` | Return test time scale. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/testing/timing.ts#L4) |
| `isAlreadyExistsError` | Error shape for is already exists. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/compat/fs.ts#L628) |
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
31 changes: 31 additions & 0 deletions src/platform/compat/process/lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import "#veryfront/schemas/_test-setup.ts";
import { assert } 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("uses Bun's synchronous builtin fallback for V8 heap statistics", () => {
if (!isBun) return;

const hostProcess = (globalThis as {
process: { getBuiltinModule?: (specifier: string) => unknown };
}).process;
const descriptor = Object.getOwnPropertyDescriptor(hostProcess, "getBuiltinModule");

try {
Object.defineProperty(hostProcess, "getBuiltinModule", {
configurable: true,
value: undefined,
});
const limit = getV8HeapSizeLimit();
assert(
typeof limit === "number" && limit > 0,
"Bun must read its heap limit when process.getBuiltinModule is unavailable",
);
} finally {
if (descriptor) Object.defineProperty(hostProcess, "getBuiltinModule", descriptor);
else delete hostProcess.getBuiltinModule;
}
});
});
32 changes: 31 additions & 1 deletion src/platform/compat/process/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import {
invokeGlobalErrorHandler,
normalizeGlobalError,
} from "./global-error.ts";
import { runtimeProcess } from "./runtime-process.ts";
import { type RuntimeProcess, runtimeProcess } from "./runtime-process.ts";

type HostRequire = (specifier: string) => unknown;

// Bun exposes this lexical binding in ESM without installing it on globalThis.
declare const require: HostRequire | undefined;

/** Get command-line arguments (cross-runtime: Deno.args or process.argv). */
export function getArgs(): string[] {
Expand Down Expand Up @@ -70,6 +75,31 @@ 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
* V8 heap statistics (e.g. browser bundles).
*/
export function getV8HeapSizeLimit(): number | undefined {
try {
const proc = runtimeProcess as
| (RuntimeProcess & { getBuiltinModule?: (id: string) => unknown })
| null;
const v8 = (proc?.getBuiltinModule?.("node:v8") ??
(IS_BUN && typeof require === "function" ? require("node:v8") : undefined)) as
Comment thread
kojiwakayama marked this conversation as resolved.
Outdated
| { 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
66 changes: 66 additions & 0 deletions src/utils/memory/profiler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "#veryfront/schemas/_test-setup.ts";
import { assert, assertEquals } from "#veryfront/testing/assert.ts";
import { afterEach, describe, it } from "#veryfront/testing/bdd.ts";
import { withEnv } from "#veryfront/testing";
import {
checkMemoryPressure,
DEFAULT_PROFILER_CRITICAL_THRESHOLD,
Expand All @@ -14,6 +15,7 @@ import {
getMemorySnapshot,
getRapidHeapGrowthEvaluation,
registerCache,
resolveEffectiveHeapLimitMB,
setHeapWarningThreshold,
startMemoryMonitoring,
stopMemoryMonitoring,
Expand Down Expand Up @@ -98,6 +100,70 @@ describe("memory/profiler", () => {
});
});

describe("heap limit honesty", () => {
it("reports the runtime heap limit, not the DENO_V8_FLAGS env string", async () => {
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,
});

assert(
effective <= 2048,
`an unverified 4096MB env limit must be clamped to the 2048MB V8 default (got ${effective})`,
);
});
Comment thread
kojiwakayama marked this conversation as resolved.

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
Loading