mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 17:36:42 +00:00
fix(dashboard): use balloon-adjusted memory percent on the Memory tile (#1847)
* fix(dashboard): use balloon-adjusted memory percent on the Memory tile When balloon fields are present, the Memory tile hero, tone, and gauge bar now use effectiveUsagePercent so they match the used/total bytes. Health and host RAM alerts still use working-set usagePercent. * fix(dashboard): align health banner and Memory tile on balloon-adjusted memory The health banner read the raw working-set percent while the Memory tile showed the balloon-adjusted one, so a hypervisor ballooning an otherwise healthy VM could flag it critical. Both now use effectiveUsagePercent, falling back to the raw percent when balloon fields are absent. Host RAM alerts keep the working-set percent: ballooned pages are reclaimed by the hypervisor and cannot be recovered by the guest on demand. Adds guarded context lines to the Memory tile: Current VM Memory (guest retained), Current pressure (effective used over retained), Balloon reclaimable, and, for ZFS nodes, Current memory in use (used plus ARC reclaimable) above ZFS ARC reclaimable. Validated with unit tests for both components including boundary cases, type checks, lint, and visual regression comparison confirming no change outside the Memory tile.
This commit is contained in:
@@ -48,9 +48,9 @@ The gauge bars (and the corresponding numeric values) pick up amber at 80% and r
|
||||
While the dashboard is loading the CPU tile reads `--` and the caption shows `collecting metrics…`; bars and sparklines render once the first sample arrives.
|
||||
|
||||
<Note>
|
||||
**ZFS hosts:** the memory tile and host RAM alerts are ZFS ARC-aware. Reclaimable ARC cache is treated as available memory rather than used, so a large ARC does not inflate the gauge or trigger false low-memory alerts. The memory tile shows the reclaimable amount as a context line when ARC stats are readable and the reclaimable amount is nonzero. See [ZFS ARC-aware host memory](/getting-started/configuration#zfs-arc-aware-host-memory) for how to expose ARC stats to a Docker install.
|
||||
**ZFS hosts:** the memory tile and host RAM alerts are ZFS ARC-aware. Reclaimable ARC cache is treated as available memory rather than used, so a large ARC does not inflate the gauge or trigger false low-memory alerts. When ARC stats are readable and the reclaimable amount is nonzero, the memory tile also shows how much raw memory is currently in use and how much of it the ARC can give back on demand. See [ZFS ARC-aware host memory](/getting-started/configuration#zfs-arc-aware-host-memory) for how to expose ARC stats to a Docker install.
|
||||
|
||||
**Virtual machines:** the memory tile shows hypervisor-ballooned memory (TrueNAS/KVM, Proxmox) as informational context. Unlike ARC, ballooned pages are host-reclaimed and the guest cannot get them back on demand, so the gauge, health verdict, and alerts continue to use the standard working-set percentage. See [VM memory ballooning](/getting-started/configuration#vm-memory-ballooning) for details.
|
||||
**Virtual machines:** when balloon data is present (TrueNAS/KVM, Proxmox), the memory tile's percentage, gauge bar, used/total figures, and the dashboard health banner all use the balloon-adjusted values so they agree with each other. The tile adds context lines for the memory currently retained by the VM (`Current VM Memory`), how heavily that retained memory is used (`Current pressure`), and what the hypervisor has reclaimed (`Balloon reclaimable`). Unlike ARC, ballooned pages are host-reclaimed and the guest cannot get them back on demand, so host RAM alerts continue to use the standard working-set percentage to catch a hypervisor squeezing a guest. See [VM memory ballooning](/getting-started/configuration#vm-memory-ballooning) for details.
|
||||
</Note>
|
||||
|
||||
## Stack health
|
||||
|
||||
@@ -81,7 +81,7 @@ Sencho checks `SENCHO_ZFS_ARCSTATS_PATH`, then `/host/proc/spl/kstat/zfs/arcstat
|
||||
|
||||
On Linux virtual machines with memory ballooning enabled (TrueNAS/KVM, Proxmox, VMware), the hypervisor can reclaim guest memory through a balloon driver. The reclaimed amount is tracked in `/proc/meminfo` on the `Balloon:` line but standard memory counters do not account for it, so a ballooned VM can appear memory-critical when the guest workload is actually healthy.
|
||||
|
||||
Sencho reads the `Balloon:` field from `/proc/meminfo` when it is available and shows the ballooned amount on the dashboard memory tile alongside an effective-usage percentage. The memory gauge, health verdict, and host RAM alerts continue to use the standard working-set percentage: unlike ZFS ARC, ballooned memory is reclaimed by the hypervisor and the guest cannot get it back on demand, so balloon data is informational context rather than a factor in alerting or health decisions. When `/proc/meminfo` is unreadable or the `Balloon:` field is absent, the behavior is unchanged.
|
||||
Sencho reads the `Balloon:` field from `/proc/meminfo` when it is available. When that data is present, the memory tile's percentage, gauge bar, used/total figures, and the dashboard health banner use the balloon-adjusted values so they agree with each other. The tile also shows how much memory the VM currently retains (`Current VM Memory`), how heavily that retained memory is used (`Current pressure`), and what the hypervisor has reclaimed (`Balloon reclaimable`). Host RAM alerts are deliberately stricter: they keep using the standard working-set percentage, because unlike ZFS ARC, ballooned memory is reclaimed by the hypervisor and the guest cannot get it back on demand, so a hypervisor squeezing a guest must still raise an alert even when the adjusted numbers look healthy. When `/proc/meminfo` is unreadable or the `Balloon:` field is absent, the behavior is unchanged.
|
||||
|
||||
`/proc/meminfo` is usually visible inside the container at `/proc/meminfo` with no extra configuration. If your runtime does not expose it, mount it read-only:
|
||||
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ResourceGauges } from './ResourceGauges';
|
||||
import type { SystemStats } from './types';
|
||||
|
||||
const GiB = 1024 ** 3;
|
||||
const MiB = 1024 ** 2;
|
||||
|
||||
function baseStats(memory: SystemStats['memory']): SystemStats {
|
||||
return {
|
||||
cpu: { usage: '4.0', cores: 8 },
|
||||
memory,
|
||||
disk: { fs: '/', mount: '/', total: 100 * GiB, used: 40 * GiB, free: 60 * GiB, usagePercent: '40' },
|
||||
};
|
||||
}
|
||||
|
||||
// Fixture mirrors the backend contract (adjustForBalloon): effectiveTotal ===
|
||||
// total and effectiveUsed === max(used - ballooned, 0). Numbers follow the
|
||||
// issue report: 753.8 MB effective of a 5.3 GB VM with 4 GB ballooned.
|
||||
function balloonedMemory(opts: { total: number; effectiveUsed: number; ballooned: number }): SystemStats['memory'] {
|
||||
const { total, effectiveUsed, ballooned } = opts;
|
||||
const used = Math.min(effectiveUsed + ballooned, total);
|
||||
return {
|
||||
total,
|
||||
used,
|
||||
free: Math.max(total - used, 0),
|
||||
usagePercent: ((used / total) * 100).toFixed(1),
|
||||
ballooned,
|
||||
effectiveUsed,
|
||||
effectiveTotal: total,
|
||||
effectiveFree: Math.min(total - effectiveUsed, total),
|
||||
effectiveUsagePercent: ((effectiveUsed / total) * 100).toFixed(1),
|
||||
};
|
||||
}
|
||||
|
||||
function memoryTile(): HTMLElement {
|
||||
const label = screen.getByText('MEMORY');
|
||||
const tile = label.parentElement;
|
||||
if (!tile) throw new Error('MEMORY tile parent missing');
|
||||
return tile;
|
||||
}
|
||||
|
||||
function memoryHero(tile: HTMLElement): HTMLElement {
|
||||
const hero = tile.querySelector('.text-2xl');
|
||||
if (!(hero instanceof HTMLElement)) throw new Error('MEMORY hero missing');
|
||||
return hero;
|
||||
}
|
||||
|
||||
function memoryBar(tile: HTMLElement): HTMLElement | null {
|
||||
return tile.querySelector('.h-1 > div');
|
||||
}
|
||||
|
||||
describe('ResourceGauges memory tile', () => {
|
||||
it('shows balloon context lines and uses the adjusted percent for hero, tone, and bar', () => {
|
||||
// Rendered values derived from this fixture: effective 13.9% -> hero 14%,
|
||||
// retained 1.3 GB, pressure 753.8 MB / 1.3 GB = 57%.
|
||||
render(
|
||||
<ResourceGauges
|
||||
systemStats={baseStats(balloonedMemory({ total: 5.3 * GiB, effectiveUsed: 753.8 * MiB, ballooned: 4 * GiB }))}
|
||||
cpuHistory={[]}
|
||||
netHistory={[]}
|
||||
historyEndAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
const hero = memoryHero(tile);
|
||||
expect(hero.textContent).toBe('14%');
|
||||
expect(hero.className).toContain('text-stat-value');
|
||||
expect(hero.className).not.toContain('text-destructive');
|
||||
expect(hero.className).not.toContain('text-warning');
|
||||
|
||||
expect(tile.textContent).toContain('753.8 MB / 5.3 GB');
|
||||
expect(tile.textContent).toMatch(/Current VM Memory: 1\.3 GB/);
|
||||
expect(tile.textContent).toMatch(/Current pressure: 57%/);
|
||||
expect(tile.textContent).toContain('Balloon reclaimable: 4 GB');
|
||||
expect(tile.textContent).not.toContain('Ballooned to host');
|
||||
expect(tile.textContent).not.toMatch(/effective\s+14%/i);
|
||||
expect(tile.textContent).not.toMatch(/NaN/);
|
||||
|
||||
const bar = memoryBar(tile);
|
||||
expect(bar).not.toBeNull();
|
||||
expect(bar?.style.width).toBe('13.9%');
|
||||
expect(bar?.style.backgroundColor).toBe('var(--brand)');
|
||||
});
|
||||
|
||||
it('hides retained-memory lines when the denominator is nonpositive but keeps the reclaimable line', () => {
|
||||
render(
|
||||
<ResourceGauges
|
||||
systemStats={baseStats(balloonedMemory({ total: 2 * GiB, effectiveUsed: 0, ballooned: 4 * GiB }))}
|
||||
cpuHistory={[]}
|
||||
netHistory={[]}
|
||||
historyEndAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
expect(tile.textContent).not.toContain('Current VM Memory:');
|
||||
expect(tile.textContent).not.toContain('Current pressure:');
|
||||
expect(tile.textContent).toContain('Balloon reclaimable: 4 GB');
|
||||
expect(tile.textContent).not.toMatch(/NaN/);
|
||||
});
|
||||
|
||||
it('treats ballooned equal to total as fully reclaimed and hides retained-memory lines', () => {
|
||||
render(
|
||||
<ResourceGauges
|
||||
systemStats={baseStats(balloonedMemory({ total: 2 * GiB, effectiveUsed: 0, ballooned: 2 * GiB }))}
|
||||
cpuHistory={[]}
|
||||
netHistory={[]}
|
||||
historyEndAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
expect(tile.textContent).not.toContain('Current VM Memory:');
|
||||
expect(tile.textContent).not.toContain('Current pressure:');
|
||||
expect(tile.textContent).toContain('Balloon reclaimable: 2 GB');
|
||||
});
|
||||
|
||||
it('renders zero pressure when the guest uses none of its retained memory', () => {
|
||||
// Retained 1.5 GiB, nothing effectively used: pressure line must read 0%.
|
||||
render(
|
||||
<ResourceGauges
|
||||
systemStats={baseStats(balloonedMemory({ total: 4 * GiB, effectiveUsed: 0, ballooned: 2.5 * GiB }))}
|
||||
cpuHistory={[]}
|
||||
netHistory={[]}
|
||||
historyEndAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
expect(tile.textContent).toMatch(/Current VM Memory: 1\.5 GB/);
|
||||
expect(tile.textContent).toMatch(/Current pressure: 0%/);
|
||||
expect(tile.textContent).toContain('Balloon reclaimable: 2.5 GB');
|
||||
expect(tile.textContent).not.toMatch(/NaN/);
|
||||
});
|
||||
|
||||
it('omits pressure but shows retained bytes when effectiveUsed is absent', () => {
|
||||
const memory = balloonedMemory({ total: 5.3 * GiB, effectiveUsed: 753.8 * MiB, ballooned: 4 * GiB });
|
||||
delete memory.effectiveUsed;
|
||||
delete memory.effectiveUsagePercent;
|
||||
render(
|
||||
<ResourceGauges systemStats={baseStats(memory)} cpuHistory={[]} netHistory={[]} historyEndAt={null} />,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
// Hero falls back to the raw working-set percent (89.4 -> 89).
|
||||
expect(memoryHero(tile).textContent).toBe('89%');
|
||||
expect(tile.textContent).toMatch(/Current VM Memory: 1\.3 GB/);
|
||||
expect(tile.textContent).not.toContain('Current pressure:');
|
||||
expect(tile.textContent).toContain('Balloon reclaimable: 4 GB');
|
||||
expect(tile.textContent).not.toMatch(/NaN/);
|
||||
});
|
||||
|
||||
it('renders ZFS raw current-memory line as used plus arcReclaimable', () => {
|
||||
render(
|
||||
<ResourceGauges
|
||||
systemStats={baseStats({
|
||||
total: 125.5 * GiB,
|
||||
used: 19.1 * GiB,
|
||||
free: 106.4 * GiB,
|
||||
usagePercent: '15.2',
|
||||
arcReclaimable: 98.1 * GiB,
|
||||
})}
|
||||
cpuHistory={[]}
|
||||
netHistory={[]}
|
||||
historyEndAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
// Raw in use is reconstructed (used + reclaimable), not repeated from used.
|
||||
expect(tile.textContent).toMatch(/Current memory in use: 117\.2 GB/);
|
||||
expect(tile.textContent).toContain('ZFS ARC reclaimable: 98.1 GB');
|
||||
expect(tile.textContent).not.toContain('Current pressure:');
|
||||
expect(tile.textContent).not.toContain('Balloon reclaimable');
|
||||
expect(tile.textContent).not.toMatch(/NaN/);
|
||||
});
|
||||
|
||||
it('renders balloon and ZFS context blocks independently', () => {
|
||||
const memory = {
|
||||
...balloonedMemory({ total: 5.3 * GiB, effectiveUsed: 753.8 * MiB, ballooned: 4 * GiB }),
|
||||
arcReclaimable: 98.1 * GiB,
|
||||
};
|
||||
render(
|
||||
<ResourceGauges systemStats={baseStats(memory)} cpuHistory={[]} netHistory={[]} historyEndAt={null} />,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
expect(tile.textContent).toMatch(/Current VM Memory: 1\.3 GB/);
|
||||
expect(tile.textContent).toMatch(/Current pressure: 57%/);
|
||||
expect(tile.textContent).toContain('Balloon reclaimable: 4 GB');
|
||||
expect(tile.textContent).toContain('ZFS ARC reclaimable: 98.1 GB');
|
||||
// used (4.7 GiB raw working set, before balloon subtraction) plus reclaimable.
|
||||
expect(tile.textContent).toMatch(/Current memory in use: 102\.8 GB/);
|
||||
});
|
||||
|
||||
it('falls back to raw usagePercent when balloon fields are absent', () => {
|
||||
render(
|
||||
<ResourceGauges
|
||||
systemStats={baseStats({
|
||||
total: 16 * GiB,
|
||||
used: 14.4 * GiB,
|
||||
free: 1.6 * GiB,
|
||||
usagePercent: '90',
|
||||
})}
|
||||
cpuHistory={[]}
|
||||
netHistory={[]}
|
||||
historyEndAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
const hero = memoryHero(tile);
|
||||
expect(hero.textContent).toBe('90%');
|
||||
expect(hero.className).toContain('text-destructive');
|
||||
expect(tile.textContent).toContain('14.4 GB / 16 GB');
|
||||
expect(tile.textContent).not.toContain('Balloon reclaimable');
|
||||
expect(tile.textContent).not.toContain('Current memory in use');
|
||||
|
||||
const bar = memoryBar(tile);
|
||||
expect(bar).not.toBeNull();
|
||||
expect(bar?.style.width).toBe('90%');
|
||||
expect(bar?.style.backgroundColor).toBe('var(--destructive)');
|
||||
});
|
||||
|
||||
it('renders a placeholder hero and no bar when stats are missing', () => {
|
||||
render(
|
||||
<ResourceGauges
|
||||
systemStats={null}
|
||||
cpuHistory={[]}
|
||||
netHistory={[]}
|
||||
historyEndAt={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const tile = memoryTile();
|
||||
expect(memoryHero(tile).textContent).toBe('--');
|
||||
expect(memoryBar(tile)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -49,13 +49,21 @@ function GaugeBar({ value, warn = 80, crit = 90 }: { value: number; warn?: numbe
|
||||
|
||||
export function ResourceGauges({ systemStats, cpuHistory, netHistory, historyEndAt }: ResourceGaugesProps) {
|
||||
const cpuVal = parseFloat(systemStats?.cpu.usage || '0');
|
||||
// Primary gauge driven by raw ARC-adjusted percent only. Ballooned memory
|
||||
// is host-reclaimed (not guest-reclaimable like ARC), so it must not mask
|
||||
// real memory pressure in the gauge color or percent hero.
|
||||
const ramVal = parseFloat(systemStats?.memory.usagePercent || '0');
|
||||
const ramEffectivePercent = systemStats?.memory.effectiveUsagePercent ?? null;
|
||||
const ramUsed = systemStats?.memory.effectiveUsed ?? systemStats?.memory.used ?? 0;
|
||||
const ramTotal = systemStats?.memory.effectiveTotal ?? systemStats?.memory.total ?? 0;
|
||||
const memory = systemStats?.memory;
|
||||
// Balloon-adjusted percent so hero, tone, bar, and used/total agree with the
|
||||
// shared health verdict. Host RAM alerts still read working-set usagePercent.
|
||||
const ramVal = parseFloat(memory?.effectiveUsagePercent ?? memory?.usagePercent ?? '0');
|
||||
const ramUsed = memory?.effectiveUsed ?? memory?.used ?? 0;
|
||||
const ramTotal = memory?.effectiveTotal ?? memory?.total ?? 0;
|
||||
const ramBallooned = memory?.ballooned ?? 0;
|
||||
const ramArcReclaimable = memory?.arcReclaimable ?? 0;
|
||||
// Memory retained by the guest after hypervisor reclaim, with pressure as
|
||||
// effective used over that retained amount. The retained line needs a
|
||||
// positive value; pressure additionally needs the balloon-adjusted numerator
|
||||
// (never raw used).
|
||||
const vmRetained = ramTotal - ramBallooned;
|
||||
// Call sites are already inside the ramBallooned > 0 block.
|
||||
const showVmRetained = vmRetained > 0;
|
||||
const diskVal = parseFloat(systemStats?.disk?.usagePercent || '0');
|
||||
|
||||
const cpuPeak = cpuHistory.length > 0 ? Math.max(...cpuHistory) : 0;
|
||||
@@ -110,15 +118,21 @@ export function ResourceGauges({ systemStats, cpuHistory, netHistory, historyEnd
|
||||
<div className="mt-1.5 font-mono text-[11px] text-stat-subtitle">
|
||||
{systemStats ? `${formatBytes(ramUsed)} / ${formatBytes(ramTotal)}` : '\u00A0'}
|
||||
</div>
|
||||
{systemStats?.memory.ballooned && systemStats.memory.ballooned > 0 ? (
|
||||
{ramBallooned > 0 ? (
|
||||
<div className="mt-1 font-mono text-[10px] text-stat-subtitle/70">
|
||||
Ballooned to host: {formatBytes(systemStats.memory.ballooned)}
|
||||
{ramEffectivePercent !== null ? ` (effective ${parseFloat(ramEffectivePercent).toFixed(0)}%)` : ''}
|
||||
{showVmRetained ? (
|
||||
<div>Current VM Memory: {formatBytes(vmRetained)}</div>
|
||||
) : null}
|
||||
{showVmRetained && memory?.effectiveUsed !== undefined ? (
|
||||
<div>Current pressure: {((memory.effectiveUsed / vmRetained) * 100).toFixed(0)}%</div>
|
||||
) : null}
|
||||
<div>Balloon reclaimable: {formatBytes(ramBallooned)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{systemStats?.memory.arcReclaimable && systemStats.memory.arcReclaimable > 0 ? (
|
||||
{memory && ramArcReclaimable > 0 ? (
|
||||
<div className="mt-1 font-mono text-[10px] text-stat-subtitle/70">
|
||||
ZFS ARC reclaimable: {formatBytes(systemStats.memory.arcReclaimable)}
|
||||
<div>Current memory in use: {formatBytes(memory.used + ramArcReclaimable)}</div>
|
||||
<div>ZFS ARC reclaimable: {formatBytes(ramArcReclaimable)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
{systemStats ? <GaugeBar value={ramVal} /> : null}
|
||||
|
||||
@@ -6,7 +6,8 @@ const stats = (over: Partial<Stats> = {}): Stats => ({
|
||||
active: 5, managed: 5, unmanaged: 0, exited: 0, total: 5, ...over,
|
||||
});
|
||||
|
||||
// deriveHealth only reads cpu.usage, memory.usagePercent and disk?.usagePercent.
|
||||
// deriveHealth only reads cpu.usage, memory.effectiveUsagePercent (falling back
|
||||
// to memory.usagePercent) and disk?.usagePercent.
|
||||
const sys = (cpu: string, ram: string, disk: string | null): SystemStats => ({
|
||||
cpu: { usage: cpu, cores: 8 },
|
||||
memory: { total: 100, used: 50, free: 50, usagePercent: ram },
|
||||
@@ -64,4 +65,50 @@ describe('deriveHealth', () => {
|
||||
expect(deriveHealth(stats(), null, []).level).toBe('healthy');
|
||||
expect(deriveHealth(stats(), sys('10', '20', null), []).level).toBe('healthy');
|
||||
});
|
||||
|
||||
it('prefers the balloon-adjusted percent so health matches the Memory tile', () => {
|
||||
const system = sys('10', '90', '30');
|
||||
const r = deriveHealth(
|
||||
stats(),
|
||||
{
|
||||
...system,
|
||||
memory: {
|
||||
...system.memory,
|
||||
ballooned: 76,
|
||||
effectiveUsed: 14,
|
||||
effectiveTotal: 100,
|
||||
effectiveUsagePercent: '14',
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(r.level).toBe('healthy');
|
||||
expect(r.reasons).toEqual(['All systems nominal']);
|
||||
});
|
||||
|
||||
it('escalates on a high balloon-adjusted percent and reports the adjusted number', () => {
|
||||
const system = sys('10', '20', '30');
|
||||
const r = deriveHealth(
|
||||
stats(),
|
||||
{
|
||||
...system,
|
||||
memory: {
|
||||
...system.memory,
|
||||
ballooned: 10,
|
||||
effectiveUsed: 95,
|
||||
effectiveTotal: 100,
|
||||
effectiveUsagePercent: '95',
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
expect(r.level).toBe('critical');
|
||||
expect(r.reasons).toContain('RAM 95%');
|
||||
});
|
||||
|
||||
it('falls back to raw usagePercent when balloon fields are absent', () => {
|
||||
const r = deriveHealth(stats(), sys('10', '90', '30'), []);
|
||||
expect(r.level).toBe('critical');
|
||||
expect(r.reasons).toContain('RAM 90%');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,12 @@ export interface HealthResult {
|
||||
// drift apart.
|
||||
export function deriveHealth(stats: Stats, systemStats: SystemStats | null, notifications: NotificationItem[]): HealthResult {
|
||||
const cpu = parseFloat(systemStats?.cpu.usage || '0');
|
||||
// Ballooned memory is NOT subtracted for health: unlike ARC, ballooned
|
||||
// pages are host-reclaimed and the guest cannot get them back on demand.
|
||||
// A ballooned VM with real memory pressure must still show degraded/critical.
|
||||
const ram = parseFloat(systemStats?.memory.usagePercent ?? '0');
|
||||
// Health follows the same balloon-adjusted percent the Memory tile shows so
|
||||
// the verdict never contradicts the gauge. Host RAM alerts (MonitorService)
|
||||
// deliberately keep the raw working-set usagePercent: unlike ARC, ballooned
|
||||
// pages are host-reclaimed and the guest cannot get them back on demand, so
|
||||
// a hypervisor squeezing a healthy-looking VM must still fire an alert.
|
||||
const ram = parseFloat(systemStats?.memory.effectiveUsagePercent ?? systemStats?.memory.usagePercent ?? '0');
|
||||
const disk = parseFloat(systemStats?.disk?.usagePercent || '0');
|
||||
const unreadErrors = notifications.filter(n => !n.is_read && n.level === 'error').length;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user