From 764b96785633a554316ff4e06e17c5a5ad2a5a7d Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 23 Sep 2026 16:46:00 +0700 Subject: [PATCH] fix(dashboard): measure V8 heap against its limit, not its current size The Health Score's heap component divided heapUsed by heapTotal, which is only what V8 has grown the heap to so far, so a healthy server read 70-90% and dragged the score down. GET /api/settings/info now also reports server.memory.heapLimit (v8 heap_size_limit, the ceiling the process dies at); the V8 Heap bar and the Health Score divide by it, falling back to heapTotal against an older server. --- client/src/lib/api.ts | 9 ++++++++- client/src/pages/Dashboard.tsx | 19 +++++++++++++------ server/__tests__/api.test.js | 8 ++++++++ server/openapi-extra/override-ops.js | 1 + server/routes/settings.js | 6 +++++- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index f7e92b0d..b0fc25ca 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -957,7 +957,14 @@ export const api = { node_version: string; platform: string; ws_connections: number; - memory: { rss: number; heapTotal: number; heapUsed: number; external: number }; + memory: { + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + /** V8's heap_size_limit; absent on older servers (fall back to heapTotal). */ + heapLimit?: number; + }; cpu_load: number[]; arch: string; total_mem: number; diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index 6e5ebdcb..aed2016c 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -132,7 +132,14 @@ interface SystemInfo { node_version: string; platform: string; ws_connections: number; - memory: { rss: number; heapTotal: number; heapUsed: number; external: number }; + memory: { + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + /** V8's heap_size_limit; absent on older servers (fall back to heapTotal). */ + heapLimit?: number; + }; cpu_load: number[]; arch: string; total_mem: number; @@ -1562,10 +1569,10 @@ function SystemHealthTab() { const memUsedPct = info.server.total_mem > 0 ? (1 - info.server.free_mem / info.server.total_mem) * 100 : 0; - const heapUsedPct = - info.server.memory.heapTotal > 0 - ? (info.server.memory.heapUsed / info.server.memory.heapTotal) * 100 - : 0; + // Against the heap LIMIT: heapTotal is only what V8 has grown to so far, so + // used/total reads 70-90% on a healthy process and dragged the health score. + const heapMax = info.server.memory.heapLimit || info.server.memory.heapTotal; + const heapUsedPct = heapMax > 0 ? (info.server.memory.heapUsed / heapMax) * 100 : 0; return { totalEntries, @@ -1701,7 +1708,7 @@ function SystemHealthTab() {
diff --git a/server/__tests__/api.test.js b/server/__tests__/api.test.js index 2dc79ba6..34909884 100644 --- a/server/__tests__/api.test.js +++ b/server/__tests__/api.test.js @@ -1896,6 +1896,14 @@ describe("Transcript cache integration", () => { assert.equal(res.body.server.version, pkg.version); }); + it("reports the V8 heap LIMIT, not just the currently allocated heapTotal", async () => { + const res = await fetch("/api/settings/info"); + const { heapUsed, heapTotal, heapLimit } = res.body.server.memory; + assert.ok(Number.isFinite(heapLimit), "heapLimit should be a number"); + // heapTotal is what V8 has grown to so far; the limit is the ceiling. + assert.ok(heapLimit >= heapTotal && heapLimit > heapUsed); + }); + it("should evict cache entry on SessionEnd", async () => { const tmpTranscript = path.join(os.tmpdir(), `test-evict-${Date.now()}.jsonl`); fs.writeFileSync( diff --git a/server/openapi-extra/override-ops.js b/server/openapi-extra/override-ops.js index a4e51d2d..6185cd8d 100644 --- a/server/openapi-extra/override-ops.js +++ b/server/openapi-extra/override-ops.js @@ -139,6 +139,7 @@ const SETTINGS_INFO_EXAMPLE = { heapTotal: 71303168, heapUsed: 58392104, external: 3211884, + heapLimit: 4345298944, arrayBuffers: 1048576, }, cpu_load: [2.13, 2.45, 2.31], diff --git a/server/routes/settings.js b/server/routes/settings.js index 15cc224f..79f9ae26 100644 --- a/server/routes/settings.js +++ b/server/routes/settings.js @@ -7,6 +7,7 @@ const { Router } = require("express"); const fs = require("fs"); const path = require("path"); const os = require("os"); +const v8 = require("v8"); const { db, stmts, DB_PATH, DEFAULT_PRICING, applyIntroPricing } = require("../db"); const { getConnectionCount } = require("../websocket"); const { transcriptCache } = require("./hooks"); @@ -121,7 +122,10 @@ router.get("/info", (req, res) => { node_version: process.version, platform: process.platform, ws_connections: getConnectionCount(), - memory: process.memoryUsage(), + // heapTotal is only what V8 has grown the heap to SO FAR, so used/total + // sits at 70-90% on a perfectly healthy process. heapLimit is the ceiling + // the process actually dies at — the denominator a "heap used %" needs. + memory: { ...process.memoryUsage(), heapLimit: v8.getHeapStatistics().heap_size_limit }, cpu_load: os.loadavg(), arch: os.arch(), total_mem: os.totalmem(),