diff --git a/src/main/services/node-specs.ts b/src/main/services/node-specs.ts index c5bcc90..3a4104c 100644 --- a/src/main/services/node-specs.ts +++ b/src/main/services/node-specs.ts @@ -31,10 +31,20 @@ import { getNode, getSSH, updateNode } from './node-manager'; * Memo schema v1 (compact JSON, ≤ 240 bytes after `specs:v1:`): * { cpu, c, cr, r, rr } * cpu – CPU model string, truncated to 64 chars - * c – total logical cores - * cr – cores reserved for the dvpn-node container - * r – total RAM (MiB) - * rr – RAM reserved for the dvpn-node container (MiB) + * c – total logical cores on the host + * cr – cores AVAILABLE to the dvpn-node container (Docker/WSL2 VM) + * r – total host RAM (MiB) + * rr – RAM AVAILABLE to the dvpn-node container (Docker/WSL2 VM, MiB) + * + * NOTE on `cr`/`rr` semantics: the node container is launched with no CPU or + * memory cap (see docker.ts HostConfig — no Memory/NanoCpus), so nothing is + * "reserved". On Windows the container runs inside the WSL2 VM, which the + * Docker engine sizes BELOW the host (default RAM = min(50% host, 8 GB)). + * `docker info` NCPU/MemTotal report that VM allocation — the real ceiling a + * container can draw from — which is why `cr`/`rr` mean "available to the + * container", not "reserved for it". On a single-purpose Linux VPS the VM == + * host, so cr==c and rr==r there. If we ever set real HostConfig limits, these + * become true reservations and the schema should bump to v2. */ const MEMO_PREFIX = 'specs:v1:'; @@ -77,9 +87,14 @@ export async function captureLocalSpecs(): Promise { return { cpu: truncateCpu(report.cpuModel), c: report.cpuCores, - // dockerOverview().ncpu is what the Docker daemon advertises as available - // to containers — the closest "reservation" signal we have without - // inspecting the running container directly. + // `cr`/`rr` = resources AVAILABLE to the container, not reserved for it. + // The node container runs with no CPU/memory cap, so its real ceiling is + // whatever the Docker engine exposes. On Windows that's the WSL2 VM + // allocation (docker info NCPU/MemTotal), which sits below the host total + // (default RAM = min(50% host, 8 GB)) — so `rr` < `r` there and the two + // fields carry distinct, meaningful info. Fall back to host totals only if + // the docker info probe failed (engine down), since on a single-purpose + // box the VM == host anyway. cr: dockerNcpu ?? report.cpuCores, r: report.memoryMb, rr: dockerMemMb ?? report.memoryMb, @@ -112,16 +127,43 @@ export async function captureRemoteSpecs(creds: SSHCredentials): Promise 0) dockerNcpu = ncpu; + if (Number.isFinite(memBytes) && memBytes > 0) { + dockerMemMb = Math.round(memBytes / (1024 * 1024)); + } + } catch (err) { + log.debug('remote docker info probe failed during specs capture', { + err: String(err), + }); + } + return { cpu: truncateCpu(cpuModel), c, - // No remote Docker reservation probe yet — assume the dvpn-node - // container can use the whole host on a single-purpose VPS, which - // is how operators actually deploy. Refine in v2 if reservations - // become a thing on remote hosts. - cr: c, + // `cr`/`rr` = resources AVAILABLE to the container (Docker engine view), + // not reserved. Falls back to host totals when the engine probe fails. + cr: dockerNcpu ?? c, r, - rr: r, + rr: dockerMemMb ?? r, }; }); } diff --git a/src/renderer/src/screens/DeployLocal.tsx b/src/renderer/src/screens/DeployLocal.tsx index c3502bc..c34bd52 100644 --- a/src/renderer/src/screens/DeployLocal.tsx +++ b/src/renderer/src/screens/DeployLocal.tsx @@ -670,8 +670,8 @@ function OnChainSpecsCard() { operator address with a specs:v1 memo. - The memo carries CPU model, total cores, RAM, and the slice reserved - for the dvpn-node container. + The memo carries CPU model, total host cores and RAM, and the cores + and RAM available to the dvpn-node container (Docker/WSL2 VM). Operator-reported — not consensus-validated. Surfaced in diff --git a/src/renderer/src/screens/ManageDocker.tsx b/src/renderer/src/screens/ManageDocker.tsx index 7feab2f..0ae1769 100644 --- a/src/renderer/src/screens/ManageDocker.tsx +++ b/src/renderer/src/screens/ManageDocker.tsx @@ -688,11 +688,13 @@ function SystemCard({ // skeleton with placeholder rows so the right column is the same height // it'll be after data arrives. if (loaded && !overview?.reachable) return null; - const ramReserved = + // Docker engine pool = resources available to containers (on Windows this is + // the WSL2 VM allocation, below the host total). Not a per-node reservation. + const ramAvailable = overview?.totalMemoryMb ? `${fmtAmount(overview.totalMemoryMb / 1024, 1)} GB` : '—'; - const coresReserved = overview?.ncpu ? String(overview.ncpu) : '—'; + const coresAvailable = overview?.ncpu ? String(overview.ncpu) : '—'; const isLinux = window.api.platform === 'linux'; const onOpenSettings = async () => { const r = await window.api.docker.openSettings(); @@ -715,24 +717,24 @@ function SystemCard({
- Resource reservations + Resources available to containers
- Bigger reservations let one node serve more concurrent users. + A bigger pool lets one node serve more concurrent users. {isLinux ? ' On Linux, edit /etc/docker/daemon.json and restart the daemon.' : ' Edit these in Docker Desktop → Settings → Resources.'} diff --git a/src/renderer/src/screens/NodeDetails.tsx b/src/renderer/src/screens/NodeDetails.tsx index d2139c7..826f4f6 100644 --- a/src/renderer/src/screens/NodeDetails.tsx +++ b/src/renderer/src/screens/NodeDetails.tsx @@ -1396,7 +1396,7 @@ function SpecsReportingPanel({ value={{`${Math.round(specs.r / 1024)} GiB`}} /> {`${Math.round(specs.rr / 1024)} GiB`}} /> diff --git a/src/renderer/src/screens/OnChainSpecs.tsx b/src/renderer/src/screens/OnChainSpecs.tsx index 0341c39..9983680 100644 --- a/src/renderer/src/screens/OnChainSpecs.tsx +++ b/src/renderer/src/screens/OnChainSpecs.tsx @@ -130,7 +130,7 @@ export function OnChainSpecs() { className="btn btn-secondary" onClick={() => void loadSystem()} disabled={refreshing} - title="Re-read CPU, RAM and Docker reservation" + title="Re-read CPU, RAM and Docker resource availability" > {refreshing ? 'Refreshing…' : 'Refresh'} @@ -318,9 +318,9 @@ function ExplainerCard() { - + 0 ? docker!.ncpu! : totalCores; - const reservedRamMb = + const availRamMb = dockerOk && Number.isFinite(docker!.totalMemoryMb) && docker!.totalMemoryMb! > 0 ? docker!.totalMemoryMb! : totalRamMb; - const coreRatio = totalCores > 0 ? reservedCores / totalCores : 0; - const ramRatio = totalRamMb > 0 ? reservedRamMb / totalRamMb : 0; + const coreRatio = totalCores > 0 ? availCores / totalCores : 0; + const ramRatio = totalRamMb > 0 ? availRamMb / totalRamMb : 0; const totalRamGb = totalRamMb / 1024; - const reservedRamGb = reservedRamMb / 1024; + const availRamGb = availRamMb / 1024; const fmtRam = (mb: number, gb: number) => mb > 0 ? `${gb.toFixed(1)} GB` : '—'; return ( @@ -576,19 +578,19 @@ function SpecsSnapshotCard({
- {/* Reserved / total meters */} + {/* Available-to-container / host-total meters */}
@@ -601,13 +603,13 @@ function SpecsSnapshotCard({ function SpecsMeter({ icon, label, - reserved, + available, total, ratio, }: { icon: string; label: string; - reserved: string; + available: string; total: string; ratio: number; }) { @@ -629,14 +631,14 @@ function SpecsMeter({ className="text-[8.5px] uppercase tracking-[0.12em]" style={{ color: 'var(--text-dim)' }} > - Reserved / Total + Available / Total - {reserved} + {available} / {total} @@ -726,17 +728,17 @@ function SpecsMemoCard({ style={{ color: 'var(--text-muted)' }} > - + - +
diff --git a/src/renderer/src/screens/System.tsx b/src/renderer/src/screens/System.tsx index 6fa9a7f..23e668d 100644 --- a/src/renderer/src/screens/System.tsx +++ b/src/renderer/src/screens/System.tsx @@ -481,9 +481,12 @@ function DockerLimitsRow({ report: LocalSystemReport; }) { const reachable = !!docker && docker.reachable; - const reservedRamMb = + // Docker engine pool = resources available to containers (on Windows this is + // the WSL2 VM allocation, which sits below the host total). Not "reserved" — + // the node container itself runs uncapped. + const availRamMb = reachable && Number.isFinite(docker!.totalMemoryMb) ? docker!.totalMemoryMb! : 0; - const reservedCores = + const availCores = reachable && Number.isFinite(docker!.ncpu) ? docker!.ncpu! : 0; const totalRamMb = Number.isFinite(report.memoryMb) ? report.memoryMb : 0; const totalCores = Number.isFinite(report.cpuCores) ? report.cpuCores : 0; @@ -505,21 +508,21 @@ function DockerLimitsRow({
@@ -530,7 +533,7 @@ function DockerLimitsRow({ function DockerLimitCell({ label, - reserved, + value, total, unit, decimals, @@ -539,7 +542,7 @@ function DockerLimitCell({ divider, }: { label: string; - reserved: number; + value: number; total: number; unit: string; decimals: number; @@ -550,8 +553,8 @@ function DockerLimitCell({ const fmt = (n: number) => Number.isFinite(n) && n > 0 ? n.toFixed(decimals) : '—'; const pct = - total > 0 && reserved > 0 - ? Math.max(0, Math.min(100, (reserved / total) * 100)) + total > 0 && value > 0 + ? Math.max(0, Math.min(100, (value / total) * 100)) : 0; return (
- {fmt(reserved)} + {fmt(value)} ({ app: { @@ -30,7 +71,7 @@ afterEach(async () => { delete process.env['SENTINEL_TEST_USERDATA']; }); -describe('metrics store', () => { +describeMaybe('metrics store', () => { it('records and queries samples for a node within the window', async () => { const { recordSample, history } = await import('../../src/main/services/metrics'); const now = Date.now();