fix provider process memory accounting
This commit is contained in:
+1
-1
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"components/provider-gateway": {
|
||||
"name": "@unidesk/provider-gateway",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.23",
|
||||
},
|
||||
"components/shared": {
|
||||
"name": "@unidesk/shared",
|
||||
|
||||
@@ -845,7 +845,7 @@ function NodeMonitorPage({ nodes, systemStatuses, tasks, onRaw, refresh }: AnyRe
|
||||
h("article", null, h("b", null, "CPU"), h("span", null, "从 /proc/stat 计算相邻采样差值,首个采样用 load/cores 近似")),
|
||||
h("article", null, h("b", null, "Memory"), h("span", null, "实际内存 = MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,不把 page cache / buffer 计入占用")),
|
||||
h("article", null, h("b", null, "Disk"), h("span", null, "使用 df -PB1 对配置路径采样,默认监控根文件系统")),
|
||||
h("article", null, h("b", null, "Process"), h("span", null, "从 /proc/[pid] 采集进程 CPU、实际内存 RSS、线程数和磁盘 I/O 速率;表格默认按内存占用降序")),
|
||||
h("article", null, h("b", null, "Process"), h("span", null, "从 /proc/[pid] 采集进程 CPU、实际内存 PSS、RSS、线程数和磁盘 I/O 速率;PSS 不重复计算共享内存,表格默认按内存占用降序")),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -855,8 +855,12 @@ function NodeMonitorPage({ nodes, systemStatuses, tasks, onRaw, refresh }: AnyRe
|
||||
|
||||
type ProcessSortKey = "memory" | "cpu" | "disk" | "pid" | "name" | "user" | "threads" | "runtime";
|
||||
|
||||
function processMemoryBytes(row: AnyRecord): number {
|
||||
return asNumber(row.memoryBytes, asNumber(row.pssBytes, asNumber(row.rssBytes)));
|
||||
}
|
||||
|
||||
function processSortValue(row: AnyRecord, key: ProcessSortKey): string | number {
|
||||
if (key === "memory") return asNumber(row.rssBytes);
|
||||
if (key === "memory") return processMemoryBytes(row);
|
||||
if (key === "cpu") return asNumber(row.cpuPercent);
|
||||
if (key === "disk") return asNumber(row.readBytesPerSecond) + asNumber(row.writeBytesPerSecond);
|
||||
if (key === "pid") return asNumber(row.pid);
|
||||
@@ -879,6 +883,8 @@ function ProcessResourceTable({ current, onRaw }: AnyRecord) {
|
||||
const contextLoading = React.useContext(LoadingContext);
|
||||
const processSummary = current?.processSummary && typeof current.processSummary === "object" ? current.processSummary : {};
|
||||
const processes = Array.isArray(current?.processes) ? current.processes : [];
|
||||
const memoryMode = String(processSummary.memoryMode || "");
|
||||
const memoryModeLabel = memoryMode === "pss_smaps_rollup" ? "PSS" : "RSS fallback";
|
||||
const rows = useMemo(() => {
|
||||
const direction = sort.direction === "asc" ? 1 : -1;
|
||||
return [...processes].sort((left: AnyRecord, right: AnyRecord) => {
|
||||
@@ -913,6 +919,7 @@ function ProcessResourceTable({ current, onRaw }: AnyRecord) {
|
||||
),
|
||||
h("div", { className: "process-resource-actions" },
|
||||
h("span", { className: "data-chip" }, "默认按内存排序"),
|
||||
h("span", { className: "data-chip" }, `内存口径 ${memoryModeLabel}`),
|
||||
h("span", { className: "data-chip" }, `${asNumber(processSummary.visible, rows.length)} / ${asNumber(processSummary.total, rows.length)} 进程`),
|
||||
h(RawButton, { title: "Process Resource Snapshot", data: { processSummary, processes }, onOpen: onRaw, testId: "raw-process-resources" }),
|
||||
),
|
||||
@@ -927,17 +934,18 @@ function ProcessResourceTable({ current, onRaw }: AnyRecord) {
|
||||
h("th", null, "状态"),
|
||||
sortHeader("CPU", "cpu"),
|
||||
sortHeader("内存", "memory"),
|
||||
h("th", null, "RSS"),
|
||||
h("th", null, "PSS / RSS"),
|
||||
sortHeader("磁盘 I/O", "disk"),
|
||||
sortHeader("线程", "threads"),
|
||||
sortHeader("运行时长", "runtime"),
|
||||
)),
|
||||
h("tbody", null, rows.map((row: AnyRecord) => {
|
||||
const diskRate = asNumber(row.readBytesPerSecond) + asNumber(row.writeBytesPerSecond);
|
||||
const memoryBytes = processMemoryBytes(row);
|
||||
return h("tr", {
|
||||
key: `${row.pid}-${row.startedAt}`,
|
||||
"data-testid": `process-row-${safeId(row.pid)}`,
|
||||
"data-memory-bytes": String(asNumber(row.rssBytes)),
|
||||
"data-memory-bytes": String(memoryBytes),
|
||||
"data-cpu-percent": String(asNumber(row.cpuPercent)),
|
||||
"data-disk-bps": String(diskRate),
|
||||
"data-pid": String(asNumber(row.pid)),
|
||||
@@ -953,7 +961,12 @@ function ProcessResourceTable({ current, onRaw }: AnyRecord) {
|
||||
h("td", null, h("span", { className: `process-state state-${safeId(row.state || "unknown")}` }, row.state || "?")),
|
||||
h("td", null, h(ProcessMeter, { value: row.cpuPercent, label: fmtLoosePercent(row.cpuPercent), tone: "cpu" })),
|
||||
h("td", null, h(ProcessMeter, { value: row.memoryPercent, label: fmtPercent(row.memoryPercent), tone: "memory" })),
|
||||
h("td", null, fmtBytes(row.rssBytes)),
|
||||
h("td", null,
|
||||
h("div", { className: "process-io-cell" },
|
||||
h("strong", null, fmtBytes(memoryBytes)),
|
||||
h("span", null, `RSS ${fmtBytes(row.rssBytes)}`),
|
||||
),
|
||||
),
|
||||
h("td", null, h("div", { className: "process-io-cell" },
|
||||
h("strong", null, fmtBytesRate(diskRate)),
|
||||
h("span", null, `R ${fmtBytesRate(row.readBytesPerSecond)} / W ${fmtBytesRate(row.writeBytesPerSecond)}`),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unidesk/provider-gateway",
|
||||
"version": "0.2.22",
|
||||
"version": "0.2.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -914,6 +914,41 @@ function readProcessIo(pid: number): { readBytes: number; writeBytes: number } {
|
||||
}
|
||||
}
|
||||
|
||||
function readProcessMemory(pid: number, rssBytes: number): {
|
||||
memoryBytes: number;
|
||||
memoryMode: string;
|
||||
pssBytes?: number;
|
||||
privateBytes?: number;
|
||||
sharedBytes?: number;
|
||||
swapPssBytes?: number;
|
||||
} {
|
||||
try {
|
||||
const values = new Map<string, number>();
|
||||
for (const line of readFileSync(`/proc/${pid}/smaps_rollup`, "utf8").split("\n")) {
|
||||
const match = line.match(/^([A-Za-z_]+):\s+(\d+)\s+kB/);
|
||||
if (!match) continue;
|
||||
values.set(match[1], Number(match[2]) * 1024);
|
||||
}
|
||||
const pssBytes = values.get("Pss");
|
||||
if (typeof pssBytes !== "number" || !Number.isFinite(pssBytes) || pssBytes < 0) {
|
||||
throw new Error("missing Pss");
|
||||
}
|
||||
const privateBytes = (values.get("Private_Clean") ?? 0) + (values.get("Private_Dirty") ?? 0);
|
||||
const sharedBytes = (values.get("Shared_Clean") ?? 0) + (values.get("Shared_Dirty") ?? 0);
|
||||
const swapPssBytes = values.get("SwapPss") ?? 0;
|
||||
return {
|
||||
memoryBytes: pssBytes,
|
||||
memoryMode: "pss_smaps_rollup",
|
||||
pssBytes,
|
||||
privateBytes,
|
||||
sharedBytes,
|
||||
swapPssBytes,
|
||||
};
|
||||
} catch {
|
||||
return { memoryBytes: rssBytes, memoryMode: "rss_fallback" };
|
||||
}
|
||||
}
|
||||
|
||||
function readUptimeSeconds(): number {
|
||||
const uptime = Number(readFileSync("/proc/uptime", "utf8").trim().split(/\s+/)[0]);
|
||||
return Number.isFinite(uptime) ? uptime : 0;
|
||||
@@ -989,6 +1024,7 @@ async function collectProcessResources(totalMemoryBytes: number, cpuCores: numbe
|
||||
const readBytesPerSecond = previous && ioSeconds > 0 ? Math.max(0, io.readBytes - previous.readBytes) / ioSeconds : 0;
|
||||
const writeBytesPerSecond = previous && ioSeconds > 0 ? Math.max(0, io.writeBytes - previous.writeBytes) / ioSeconds : 0;
|
||||
const rssBytes = Math.max(0, stat.rssPages * pageSize);
|
||||
const processMemory = readProcessMemory(pid, rssBytes);
|
||||
const name = status.name || stat.name;
|
||||
const uid = status.uid >= 0 ? status.uid : 0;
|
||||
rows.push({
|
||||
@@ -1000,8 +1036,14 @@ async function collectProcessResources(totalMemoryBytes: number, cpuCores: numbe
|
||||
command: readProcessCommand(pid, name),
|
||||
state: stat.state,
|
||||
cpuPercent: roundMetric(Math.min(cpuPercent, Math.max(1, cpuCores) * 100)),
|
||||
memoryPercent: totalMemoryBytes > 0 ? clampPercent((rssBytes / totalMemoryBytes) * 100) : 0,
|
||||
memoryPercent: totalMemoryBytes > 0 ? clampPercent((processMemory.memoryBytes / totalMemoryBytes) * 100) : 0,
|
||||
memoryBytes: processMemory.memoryBytes,
|
||||
memoryMode: processMemory.memoryMode,
|
||||
rssBytes,
|
||||
pssBytes: processMemory.pssBytes,
|
||||
privateBytes: processMemory.privateBytes,
|
||||
sharedBytes: processMemory.sharedBytes,
|
||||
swapPssBytes: processMemory.swapPssBytes,
|
||||
vmsBytes: Math.max(0, stat.vmsBytes),
|
||||
threads: status.threads || stat.threads,
|
||||
readBytes: io.readBytes,
|
||||
@@ -1024,7 +1066,8 @@ async function collectProcessResources(totalMemoryBytes: number, cpuCores: numbe
|
||||
}
|
||||
|
||||
previousProcessSamples = new Map([...previousProcessSamples].filter(([pid]) => seen.has(pid)));
|
||||
rows.sort((a, b) => b.rssBytes - a.rssBytes || b.cpuPercent - a.cpuPercent || a.pid - b.pid);
|
||||
rows.sort((a, b) => b.memoryBytes - a.memoryBytes || b.cpuPercent - a.cpuPercent || a.pid - b.pid);
|
||||
const pssRows = rows.filter((row) => row.memoryMode === "pss_smaps_rollup").length;
|
||||
return {
|
||||
processes: rows.slice(0, 120),
|
||||
summary: {
|
||||
@@ -1033,6 +1076,9 @@ async function collectProcessResources(totalMemoryBytes: number, cpuCores: numbe
|
||||
skipped,
|
||||
defaultSort: "memory_desc",
|
||||
scope: "provider_pid_namespace",
|
||||
memoryMode: pssRows > 0 ? "pss_smaps_rollup" : "rss_fallback",
|
||||
pssRows,
|
||||
rssFallbackRows: rows.length - pssRows,
|
||||
cpuPercentMode: hasPreviousProcessSample ? "delta_ticks_per_sample" : "lifetime_average_first_sample",
|
||||
diskIoMode: "proc_pid_io_delta_bytes_per_second",
|
||||
},
|
||||
|
||||
@@ -28,7 +28,13 @@ export interface ProcessResourceSummary {
|
||||
state: string;
|
||||
cpuPercent: number;
|
||||
memoryPercent: number;
|
||||
memoryBytes: number;
|
||||
memoryMode: string;
|
||||
rssBytes: number;
|
||||
pssBytes?: number;
|
||||
privateBytes?: number;
|
||||
sharedBytes?: number;
|
||||
swapPssBytes?: number;
|
||||
vmsBytes: number;
|
||||
threads: number;
|
||||
readBytes: number;
|
||||
|
||||
Reference in New Issue
Block a user