mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
memory: restore the v5 reclaimable-cache split end-to-end
v5 modeled memory as used | cache | free (Memory.Cache, 'reclaimable buff/cache') and the memory bar's tooltip carried a 'Shown in Proxmox' row explaining why Pulse's percentage reads lower than the Proxmox UI's cache-inclusive number — a recurring support question. The v6 rebuild deleted the field from the backend model, so the split and the reconciliation vanished product-wide. Flagged by the Proxmox overview parity audit. Backend: re-add Memory.Cache; split it out via a shared splitReclaimableMemory helper at the node resolver (node status reports truly-free directly) and the VM builder (when guest meminfo reported free pages); transport as proxmox.memoryCache on unified resources alongside swap/balloon; mock mode populates plausible cache for nodes and VMs. Frontend: cache prop on StackedMemoryBar with the v5 muted-amber segment between active and balloon, tooltip rows for reclaimable cache, truly-free Free (balloon-capped), and the 'Shown in Proxmox' reconciliation; guest and node memory adapters normalize free to truly-free at the boundary; guest and node drawers grow a Reclaimable cache row; the Proxmox nodes table passes cache and node swap through.
This commit is contained in:
parent
40058fbc49
commit
f62f35e24d
18 changed files with 263 additions and 30 deletions
|
|
@ -351,6 +351,7 @@ export function GuestRow(props: GuestRowProps) {
|
|||
used={props.guest.memory?.used || 0}
|
||||
total={props.guest.memory?.total || 0}
|
||||
percentOnly={memoryPercentOnly()}
|
||||
cache={props.guest.memory?.cache || 0}
|
||||
balloon={props.guest.memory?.balloon || 0}
|
||||
swapUsed={props.guest.memory?.swapUsed || 0}
|
||||
swapTotal={props.guest.memory?.swapTotal || 0}
|
||||
|
|
|
|||
|
|
@ -246,6 +246,14 @@ export function NodeDrawerOverview(props: NodeDrawerOverviewProps) {
|
|||
)}`,
|
||||
},
|
||||
{ label: 'Total', value: formatBytes(props.node.memory?.total || 0) },
|
||||
...(props.node.memory?.cache
|
||||
? [
|
||||
{
|
||||
label: 'Reclaimable cache',
|
||||
value: formatBytes(props.node.memory.cache),
|
||||
} satisfies NodeOverviewRow,
|
||||
]
|
||||
: []),
|
||||
{ label: 'Free', value: formatBytes(props.node.memory?.free || 0) },
|
||||
...(props.node.memory?.swapTotal
|
||||
? [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildStackedMemoryBarPresentation } from '../stackedMemoryBarModel';
|
||||
|
||||
const GiB = 1024 ** 3;
|
||||
|
||||
describe('buildStackedMemoryBarPresentation', () => {
|
||||
it('renders the v5 used | reclaimable cache split with the Proxmox reconciliation row', () => {
|
||||
const presentation = buildStackedMemoryBarPresentation(
|
||||
{ used: 4 * GiB, total: 16 * GiB, cache: 6 * GiB },
|
||||
400,
|
||||
);
|
||||
|
||||
expect(presentation.segments.map((segment) => segment.label)).toEqual([
|
||||
'Active',
|
||||
'Reclaimable',
|
||||
]);
|
||||
expect(presentation.segments[1].leftPercent).toBeCloseTo(25);
|
||||
expect(presentation.segments[1].widthPercent).toBeCloseTo(37.5);
|
||||
|
||||
const rows = Object.fromEntries(presentation.tooltipRows.map((row) => [row.label, row.value]));
|
||||
expect(rows['Used']).toBe('4.00 GB');
|
||||
expect(rows['Reclaimable cache']).toBe('6.00 GB');
|
||||
// Truly free excludes the reclaimable cache: 16 - 4 - 6.
|
||||
expect(rows['Free']).toBe('6.00 GB');
|
||||
// Proxmox counts cache as used: (4 + 6) / 16.
|
||||
expect(rows['Shown in Proxmox']).toBe('63%');
|
||||
});
|
||||
|
||||
it('keeps the cache segment between active and the balloon limit', () => {
|
||||
const presentation = buildStackedMemoryBarPresentation(
|
||||
{ used: 4 * GiB, total: 16 * GiB, cache: 2 * GiB, balloon: 8 * GiB },
|
||||
400,
|
||||
);
|
||||
|
||||
expect(presentation.segments.map((segment) => segment.label)).toEqual([
|
||||
'Active',
|
||||
'Reclaimable',
|
||||
'Balloon',
|
||||
]);
|
||||
const balloonSegment = presentation.segments[2];
|
||||
expect(balloonSegment.leftPercent).toBeCloseTo(37.5);
|
||||
expect(balloonSegment.widthPercent).toBeCloseTo(12.5);
|
||||
|
||||
const rows = Object.fromEntries(presentation.tooltipRows.map((row) => [row.label, row.value]));
|
||||
// Ballooning caps the usable ceiling: free = 8 - 4 - 2.
|
||||
expect(rows['Free']).toBe('2.00 GB');
|
||||
});
|
||||
|
||||
it('matches the pre-cache layout when no cache is reported', () => {
|
||||
const presentation = buildStackedMemoryBarPresentation(
|
||||
{ used: 4 * GiB, total: 16 * GiB },
|
||||
400,
|
||||
);
|
||||
|
||||
expect(presentation.segments.map((segment) => segment.label)).toEqual(['Active']);
|
||||
const labels = presentation.tooltipRows.map((row) => row.label);
|
||||
expect(labels).toEqual(['Used', 'Free']);
|
||||
const rows = Object.fromEntries(presentation.tooltipRows.map((row) => [row.label, row.value]));
|
||||
expect(rows['Free']).toBe('12.0 GB');
|
||||
});
|
||||
});
|
||||
|
|
@ -298,10 +298,14 @@ export const getGuestDrawerMemoryRows = (guest: Guest): GuestDrawerMemoryRow[] =
|
|||
const rows: GuestDrawerMemoryRow[] = [];
|
||||
const total = memory.total ?? 0;
|
||||
const used = memory.used ?? 0;
|
||||
const cache = memory.cache ?? 0;
|
||||
|
||||
if (total > 0) {
|
||||
rows.push({ label: 'Usage', value: `${formatPercent((used / total) * 100)} · ${formatBytes(used)}` });
|
||||
rows.push({ label: 'Total', value: formatBytes(total) });
|
||||
if (cache > 0) {
|
||||
rows.push({ label: 'Reclaimable cache', value: formatBytes(cache) });
|
||||
}
|
||||
if (typeof memory.free === 'number') {
|
||||
rows.push({ label: 'Free', value: formatBytes(memory.free) });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export interface StackedMemoryBarProps {
|
|||
used: number;
|
||||
total: number;
|
||||
percentOnly?: number;
|
||||
/** Reclaimable buff/cache (available - truly free); used + cache + free ≈ total. */
|
||||
cache?: number;
|
||||
swapUsed?: number;
|
||||
swapTotal?: number;
|
||||
balloon?: number;
|
||||
|
|
@ -52,6 +54,8 @@ export interface StackedMemoryBarPresentation {
|
|||
|
||||
const MEMORY_COLORS = {
|
||||
active: 'rgba(34, 197, 94, 0.6)',
|
||||
// Muted amber: reclaimable buff/cache, matching the v5 segment tone.
|
||||
cache: 'rgba(251, 191, 36, 0.45)',
|
||||
balloon: 'rgba(59, 130, 246, 0.6)',
|
||||
swap: 'rgba(168, 85, 247, 0.6)',
|
||||
};
|
||||
|
|
@ -87,20 +91,8 @@ function getSegments(
|
|||
const balloon = props.balloon || 0;
|
||||
const hasActiveBallooning = balloon > 0 && balloon < props.total;
|
||||
const usedPercent = (props.used / props.total) * 100;
|
||||
|
||||
if (!hasActiveBallooning) {
|
||||
if (props.used <= 0) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
color: getMetricColorRgba(usedPercent, 'memory', props.thresholds),
|
||||
label: 'Active',
|
||||
leftPercent: 0,
|
||||
widthPercent: usedPercent,
|
||||
},
|
||||
];
|
||||
}
|
||||
const cache = props.cache || 0;
|
||||
const cachePercent = (cache / props.total) * 100;
|
||||
|
||||
const segments: StackedMemorySegment[] = [];
|
||||
if (props.used > 0) {
|
||||
|
|
@ -112,16 +104,32 @@ function getSegments(
|
|||
});
|
||||
}
|
||||
|
||||
const balloonLimitPercent = Math.max(0, (balloon / props.total) * 100 - usedPercent);
|
||||
if (balloonLimitPercent > 0 && balloon > props.used) {
|
||||
// Reclaimable buff/cache rides between active and the balloon limit, like v5.
|
||||
if (cache > 0) {
|
||||
segments.push({
|
||||
color: MEMORY_COLORS.balloon,
|
||||
label: 'Balloon',
|
||||
color: MEMORY_COLORS.cache,
|
||||
label: 'Reclaimable',
|
||||
leftPercent: usedPercent,
|
||||
widthPercent: balloonLimitPercent,
|
||||
widthPercent: cachePercent,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasActiveBallooning) {
|
||||
const usedPlusCache = props.used + cache;
|
||||
const balloonLimitPercent = Math.max(
|
||||
0,
|
||||
(balloon / props.total) * 100 - usedPercent - cachePercent,
|
||||
);
|
||||
if (balloonLimitPercent > 0 && balloon > usedPlusCache) {
|
||||
segments.push({
|
||||
color: MEMORY_COLORS.balloon,
|
||||
label: 'Balloon',
|
||||
leftPercent: usedPercent + cachePercent,
|
||||
widthPercent: balloonLimitPercent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
|
|
@ -131,6 +139,7 @@ function getTooltipRows(
|
|||
): StackedMemoryTooltipRow[] {
|
||||
const rows: StackedMemoryTooltipRow[] = [];
|
||||
const balloon = props.balloon || 0;
|
||||
const cache = props.cache || 0;
|
||||
const hasActiveBallooning = props.total > 0 && balloon > 0 && balloon < props.total;
|
||||
const hasSwap = (props.swapTotal || 0) > 0;
|
||||
|
||||
|
|
@ -142,6 +151,15 @@ function getTooltipRows(
|
|||
value: formatBytes(props.used),
|
||||
});
|
||||
|
||||
if (cache > 0) {
|
||||
rows.push({
|
||||
borderTop: true,
|
||||
label: 'Reclaimable cache',
|
||||
labelClass: 'text-amber-400',
|
||||
value: formatBytes(cache),
|
||||
});
|
||||
}
|
||||
|
||||
if (hasActiveBallooning) {
|
||||
rows.push({
|
||||
borderTop: true,
|
||||
|
|
@ -151,12 +169,26 @@ function getTooltipRows(
|
|||
});
|
||||
}
|
||||
|
||||
// Truly free pages exclude the reclaimable cache; capped at the balloon
|
||||
// limit when ballooning is active (the guest cannot use past it).
|
||||
const ceiling = hasActiveBallooning ? balloon : props.total;
|
||||
rows.push({
|
||||
borderTop: true,
|
||||
label: 'Free',
|
||||
labelClass: 'text-slate-400',
|
||||
value: formatBytes(props.total - props.used),
|
||||
value: formatBytes(Math.max(0, ceiling - props.used - cache)),
|
||||
});
|
||||
|
||||
// Proxmox's UI counts reclaimable cache as used; this row explains why
|
||||
// Pulse's percentage reads lower than the same guest in Proxmox.
|
||||
if (cache > 0) {
|
||||
rows.push({
|
||||
borderTop: true,
|
||||
label: 'Shown in Proxmox',
|
||||
labelClass: 'text-slate-500 italic',
|
||||
value: formatPercent(((props.used + cache) / props.total) * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
rows.push({
|
||||
borderTop: true,
|
||||
|
|
|
|||
|
|
@ -474,6 +474,9 @@ export const ProxmoxNodesTable: Component<{
|
|||
used={memoryUsed()}
|
||||
total={memoryTotal()}
|
||||
percentOnly={memoryPercentOnly()}
|
||||
cache={drawerNode()?.memory?.cache || 0}
|
||||
swapUsed={drawerNode()?.memory?.swapUsed || 0}
|
||||
swapTotal={drawerNode()?.memory?.swapTotal || 0}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ type APIResource = {
|
|||
swapUsed?: number;
|
||||
swapTotal?: number;
|
||||
balloon?: number;
|
||||
memoryCache?: number;
|
||||
isOci?: boolean;
|
||||
osTemplate?: string;
|
||||
osName?: string;
|
||||
|
|
@ -455,8 +456,13 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => {
|
|||
cpus: resource.proxmox?.cpus ?? 1,
|
||||
memory: (() => {
|
||||
const base = buildMetric(resource.metrics?.memory);
|
||||
const cache = resource.proxmox?.memoryCache ?? 0;
|
||||
return {
|
||||
...base,
|
||||
// buildMetric derives free as total-used (available); carve the
|
||||
// reclaimable cache back out so free means truly-free pages.
|
||||
free: Math.max(0, base.free - cache),
|
||||
cache,
|
||||
swapUsed: resource.proxmox?.swapUsed ?? 0,
|
||||
swapTotal: resource.proxmox?.swapTotal ?? 0,
|
||||
balloon: resource.proxmox?.balloon ?? 0,
|
||||
|
|
|
|||
|
|
@ -1000,6 +1000,8 @@ export interface Memory {
|
|||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
/** Reclaimable buff/cache (available - truly free); used + cache + free ≈ total. */
|
||||
cache?: number;
|
||||
usage: number;
|
||||
balloon?: number;
|
||||
swapUsed?: number;
|
||||
|
|
|
|||
|
|
@ -617,6 +617,9 @@ export interface ResourceProxmoxMeta {
|
|||
swapUsed?: number;
|
||||
swapTotal?: number;
|
||||
balloon?: number;
|
||||
/** Reclaimable buff/cache split out of the memory metric's free bytes. */
|
||||
memoryCache?: number;
|
||||
pendingUpdates?: number;
|
||||
isOci?: boolean;
|
||||
osTemplate?: string;
|
||||
pveVersion?: string;
|
||||
|
|
|
|||
|
|
@ -774,10 +774,18 @@ export const mergeCanonicalResourceSnapshot = (
|
|||
);
|
||||
};
|
||||
|
||||
const buildMemory = (metric: Resource['memory'], fallback?: Record<string, unknown>): Memory => {
|
||||
const buildMemory = (
|
||||
metric: Resource['memory'],
|
||||
fallback?: Record<string, unknown>,
|
||||
proxmoxMeta?: Record<string, unknown>,
|
||||
): Memory => {
|
||||
const total = metric?.total ?? asNumber(fallback?.total) ?? 0;
|
||||
const used = metric?.used ?? asNumber(fallback?.used) ?? 0;
|
||||
const free = metric?.free ?? asNumber(fallback?.free) ?? Math.max(total - used, 0);
|
||||
const cache = asNumber(proxmoxMeta?.memoryCache) ?? asNumber(fallback?.cache) ?? 0;
|
||||
// The metric ships no free bytes for PVE payloads; total-used is the
|
||||
// reclaimable-inclusive available, so carve the cache back out when known.
|
||||
const free =
|
||||
metric?.free ?? asNumber(fallback?.free) ?? Math.max(total - used - cache, 0);
|
||||
const usage =
|
||||
metric?.current ?? (total > 0 ? (used / total) * 100 : (asNumber(fallback?.usage) ?? 0));
|
||||
return {
|
||||
|
|
@ -785,8 +793,9 @@ const buildMemory = (metric: Resource['memory'], fallback?: Record<string, unkno
|
|||
used,
|
||||
free,
|
||||
usage,
|
||||
swapUsed: asNumber(fallback?.swapUsed),
|
||||
swapTotal: asNumber(fallback?.swapTotal),
|
||||
cache: cache > 0 ? cache : undefined,
|
||||
swapUsed: asNumber(proxmoxMeta?.swapUsed) ?? asNumber(fallback?.swapUsed),
|
||||
swapTotal: asNumber(proxmoxMeta?.swapTotal) ?? asNumber(fallback?.swapTotal),
|
||||
balloon: asNumber(fallback?.balloon),
|
||||
};
|
||||
};
|
||||
|
|
@ -933,7 +942,7 @@ export const nodeFromResource = (resource: Resource): Node | null => {
|
|||
status: resource.status || 'unknown',
|
||||
type: resource.type,
|
||||
cpu: resource.cpu?.current ?? 0,
|
||||
memory: buildMemory(resource.memory, asRecord(proxmox?.memory)),
|
||||
memory: buildMemory(resource.memory, asRecord(proxmox?.memory), proxmox),
|
||||
disk: buildDisk(resource.disk, asRecord(proxmox?.disk)),
|
||||
networkIn: resource.network?.rxBytes,
|
||||
networkOut: resource.network?.txBytes,
|
||||
|
|
|
|||
|
|
@ -873,7 +873,10 @@ func generateNode(name string, highLoad bool, config MockConfig) models.Node {
|
|||
Memory: models.Memory{
|
||||
Total: totalMem * 1024 * 1024 * 1024, // Convert to bytes
|
||||
Used: usedMem * 1024 * 1024 * 1024,
|
||||
Free: (totalMem - usedMem) * 1024 * 1024 * 1024,
|
||||
// Roughly a third of the non-used pages read as reclaimable
|
||||
// buff/cache so the node memory split is exercisable in mock mode.
|
||||
Cache: (totalMem - usedMem) / 3 * 1024 * 1024 * 1024,
|
||||
Free: ((totalMem - usedMem) - (totalMem-usedMem)/3) * 1024 * 1024 * 1024,
|
||||
Usage: float64(usedMem) / float64(totalMem) * 100,
|
||||
},
|
||||
Disk: models.Disk{
|
||||
|
|
@ -1116,6 +1119,12 @@ func generateVM(nodeName string, instance string, vmid int, config MockConfig) m
|
|||
SwapTotal: swapTotal,
|
||||
SwapUsed: swapUsed,
|
||||
}
|
||||
// Carve reclaimable buff/cache out of the free pages so the
|
||||
// used | cache | free memory split is exercisable in mock mode.
|
||||
if mem.Free > 0 {
|
||||
mem.Cache = int64(float64(mem.Free) * (0.2 + rand.Float64()*0.4))
|
||||
mem.Free -= mem.Cache
|
||||
}
|
||||
uptime = int64(3600 * (1 + rand.Intn(720))) // 1-720 hours
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ type Alert struct {
|
|||
Acknowledged bool `json:"acknowledged"`
|
||||
AckTime *time.Time `json:"ackTime,omitempty"`
|
||||
AckUser string `json:"ackUser,omitempty"`
|
||||
// Metadata carries alert-engine annotations (notably resourceType) so the
|
||||
// frontend can classify an alert without re-deriving resource identity.
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ResolvedAlert represents a recently resolved alert
|
||||
|
|
@ -2649,9 +2652,11 @@ type PMGQueueStatus struct {
|
|||
|
||||
// Memory represents memory usage
|
||||
type Memory struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Free int64 `json:"free"`
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Free int64 `json:"free"`
|
||||
// Reclaimable buff/cache (available - truly free); used + cache + free ≈ total.
|
||||
Cache int64 `json:"cache,omitempty"`
|
||||
Usage float64 `json:"usage"`
|
||||
Balloon int64 `json:"balloon,omitempty"`
|
||||
SwapUsed int64 `json:"swapUsed,omitempty"`
|
||||
|
|
|
|||
21
internal/monitoring/memory_reclaimable.go
Normal file
21
internal/monitoring/memory_reclaimable.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package monitoring
|
||||
|
||||
import "github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
|
||||
// splitReclaimableMemory splits the reclaimable page cache out of a memory
|
||||
// snapshot whose Free currently holds "available" (Total - Used, with Used
|
||||
// already excluding cache). trulyFree is the node/guest-reported free page
|
||||
// count; when it is known and smaller than Free, the gap is reclaimable
|
||||
// buff/cache and the snapshot becomes used | cache | free, which is what the
|
||||
// frontend memory bar needs to reconcile Pulse's percentage with the
|
||||
// cache-inclusive number the Proxmox UI shows.
|
||||
func splitReclaimableMemory(memory *models.Memory, trulyFree uint64) {
|
||||
if memory == nil || trulyFree == 0 {
|
||||
return
|
||||
}
|
||||
if memory.Free <= 0 || int64(trulyFree) >= memory.Free {
|
||||
return
|
||||
}
|
||||
memory.Cache = memory.Free - int64(trulyFree)
|
||||
memory.Free = int64(trulyFree)
|
||||
}
|
||||
55
internal/monitoring/memory_reclaimable_test.go
Normal file
55
internal/monitoring/memory_reclaimable_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package monitoring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
)
|
||||
|
||||
func TestSplitReclaimableMemory(t *testing.T) {
|
||||
t.Run("splits cache out of available-based free", func(t *testing.T) {
|
||||
memory := models.Memory{Total: 16, Used: 6, Free: 10, Usage: 37.5}
|
||||
splitReclaimableMemory(&memory, 4)
|
||||
if memory.Cache != 6 {
|
||||
t.Fatalf("Cache = %d, want 6", memory.Cache)
|
||||
}
|
||||
if memory.Free != 4 {
|
||||
t.Fatalf("Free = %d, want 4", memory.Free)
|
||||
}
|
||||
if memory.Used != 6 || memory.Total != 16 {
|
||||
t.Fatalf("Used/Total mutated: %+v", memory)
|
||||
}
|
||||
if got := memory.Used + memory.Cache + memory.Free; got != memory.Total {
|
||||
t.Fatalf("used+cache+free = %d, want %d", got, memory.Total)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no-op when truly free is unknown", func(t *testing.T) {
|
||||
memory := models.Memory{Total: 16, Used: 6, Free: 10}
|
||||
splitReclaimableMemory(&memory, 0)
|
||||
if memory.Cache != 0 || memory.Free != 10 {
|
||||
t.Fatalf("unexpected split: %+v", memory)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no-op when truly free is not smaller than free", func(t *testing.T) {
|
||||
memory := models.Memory{Total: 16, Used: 6, Free: 10}
|
||||
splitReclaimableMemory(&memory, 10)
|
||||
if memory.Cache != 0 || memory.Free != 10 {
|
||||
t.Fatalf("unexpected split: %+v", memory)
|
||||
}
|
||||
splitReclaimableMemory(&memory, 12)
|
||||
if memory.Cache != 0 || memory.Free != 10 {
|
||||
t.Fatalf("unexpected split with oversized trulyFree: %+v", memory)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no-op on nil or non-positive free", func(t *testing.T) {
|
||||
splitReclaimableMemory(nil, 4)
|
||||
memory := models.Memory{Total: 16, Used: 16, Free: 0}
|
||||
splitReclaimableMemory(&memory, 4)
|
||||
if memory.Cache != 0 {
|
||||
t.Fatalf("unexpected split on zero free: %+v", memory)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -332,6 +332,9 @@ func (m *Monitor) buildVMFromClusterResource(
|
|||
if memory.Used > memory.Total {
|
||||
memory.Used = memory.Total
|
||||
}
|
||||
// Free above is total-used, i.e. available. When the guest reported its
|
||||
// truly-free pages (meminfo), split the reclaimable cache back out.
|
||||
splitReclaimableMemory(&memory, state.guestRaw.MemInfoFree)
|
||||
if state.detailedStatus != nil && state.detailedStatus.Balloon > 0 {
|
||||
memory.Balloon = int64(state.detailedStatus.Balloon)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,6 +202,9 @@ func (m *Monitor) resolveNodeMemory(
|
|||
Free: free,
|
||||
Usage: safePercentage(float64(actualUsed), float64(memory.Total)),
|
||||
}
|
||||
// Free above is total-used, i.e. available. The node status reports its
|
||||
// truly-free pages directly, so split the reclaimable cache back out.
|
||||
splitReclaimableMemory(&resolved, memory.Free)
|
||||
|
||||
return resolved, source, fallbackReason, raw, true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ func resourceFromProxmoxNode(node models.Node, linkedHost *models.Host) (Resourc
|
|||
PendingUpdates: node.PendingUpdates,
|
||||
TemperatureMonitoringEnabled: cloneBoolPtr(node.TemperatureMonitoringEnabled),
|
||||
PendingUpdatesCheckedAt: zeroTimeToPtr(node.PendingUpdatesCheckedAt),
|
||||
MemoryCache: node.Memory.Cache,
|
||||
SwapUsed: node.Memory.SwapUsed,
|
||||
SwapTotal: node.Memory.SwapTotal,
|
||||
LinkedAgentID: linkedAgentID,
|
||||
}
|
||||
|
||||
|
|
@ -1509,6 +1512,7 @@ func resourceFromVM(vm models.VM) (Resource, ResourceIdentity) {
|
|||
SwapUsed: vm.Memory.SwapUsed,
|
||||
SwapTotal: vm.Memory.SwapTotal,
|
||||
Balloon: vm.Memory.Balloon,
|
||||
MemoryCache: vm.Memory.Cache,
|
||||
Lock: vm.Lock,
|
||||
}
|
||||
resource := Resource{
|
||||
|
|
@ -1553,6 +1557,7 @@ func resourceFromContainer(ct models.Container) (Resource, ResourceIdentity) {
|
|||
SwapUsed: ct.Memory.SwapUsed,
|
||||
SwapTotal: ct.Memory.SwapTotal,
|
||||
Balloon: ct.Memory.Balloon,
|
||||
MemoryCache: ct.Memory.Cache,
|
||||
Lock: ct.Lock,
|
||||
}
|
||||
resource := Resource{
|
||||
|
|
|
|||
|
|
@ -354,7 +354,9 @@ type ProxmoxData struct {
|
|||
SwapUsed int64 `json:"swapUsed,omitempty"`
|
||||
SwapTotal int64 `json:"swapTotal,omitempty"`
|
||||
Balloon int64 `json:"balloon,omitempty"`
|
||||
Lock string `json:"lock,omitempty"` // Proxmox lock state (e.g. "backup", "migrate", "snapshot")
|
||||
// Reclaimable buff/cache split out of the memory metric's free bytes.
|
||||
MemoryCache int64 `json:"memoryCache,omitempty"`
|
||||
Lock string `json:"lock,omitempty"` // Proxmox lock state (e.g. "backup", "migrate", "snapshot")
|
||||
// Internal link hint to a host agent resource.
|
||||
LinkedAgentID string `json:"-"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue