feat(monitor): shared Linux disk I/O and one-second refresh (#3870)
This commit is contained in:
21 files changed
+250
-4
No files matched your search
@@ -181,7 +181,9 @@ Organize and navigate your hosts using the view that best fits the moment: grid
|
||||
|
||||
## Monitoring View
|
||||
|
||||
Inspect CPU, memory, disk, network, and latency readings, with GPU details where available. Choose visible columns, resize them, adjust refresh controls, and open a terminal directly from a monitored host.
|
||||
Inspect CPU, memory, disk, network, and latency readings, with GPU details where available. Choose visible columns, resize them, select refresh intervals down to 1 second, and open a terminal directly from a monitored host.
|
||||
|
||||
Disk I/O shows aggregate read/write throughput for Linux hardware-backed block devices, without counting partitions or stacked virtual devices twice. Rates use two consecutive samples; `--` means unavailable or warming up, not zero activity. Disk I/O is currently unavailable on macOS; existing macOS monitoring is unchanged. The terminal system overview uses the same shared measurements.
|
||||
|
||||
<img width="3364" height="1982" alt="image" src="docs/assets/readme/monitoring.webp" />
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const enMonitorMessages = {
|
||||
"monitor.diskIo": "Disk I/O",
|
||||
"monitor.diskRead": "Read",
|
||||
"monitor.diskWrite": "Write",
|
||||
"monitor.diskIoHint": "Aggregate Linux hardware-backed block-device I/O, excluding partitions and stacked virtual devices. Unavailable until two valid samples; not supported on macOS.",
|
||||
|
||||
"monitor.actions": "Panel actions",
|
||||
"monitor.unavailable": "Monitoring is unavailable. Try starting again when the connection is restored.",
|
||||
"monitor.username": "Username",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const esMonitorMessages = {
|
||||
"monitor.diskIo": "E/S de disco",
|
||||
"monitor.diskRead": "Lectura",
|
||||
"monitor.diskWrite": "Escritura",
|
||||
"monitor.diskIoHint": "E/S agregada de dispositivos de bloques físicos de Linux, sin contar particiones ni capas virtuales. No disponible hasta tener dos muestras válidas; no compatible con macOS.",
|
||||
|
||||
"monitor.actions": "Acciones del panel",
|
||||
"monitor.unavailable": "La monitorización no está disponible. Iníciala de nuevo cuando se restablezca la conexión.",
|
||||
"monitor.username": "Usuario",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const ruMonitorMessages = {
|
||||
"monitor.diskIo": "Дисковый ввод-вывод",
|
||||
"monitor.diskRead": "Чтение",
|
||||
"monitor.diskWrite": "Запись",
|
||||
"monitor.diskIoHint": "Суммарный ввод-вывод физических блочных устройств Linux, без разделов и виртуальных слоёв. Доступен после двух корректных замеров; macOS не поддерживается.",
|
||||
|
||||
"monitor.actions": "Действия панели",
|
||||
"monitor.unavailable": "Мониторинг недоступен. Запустите его снова после восстановления связи.",
|
||||
"monitor.username": "Пользователь",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const zhCNMonitorMessages = {
|
||||
"monitor.diskIo": "磁盘 I/O",
|
||||
"monitor.diskRead": "读取",
|
||||
"monitor.diskWrite": "写入",
|
||||
"monitor.diskIoHint": "Linux 底层块设备的汇总 I/O,不重复统计分区和叠加的虚拟设备。取得两次有效采样前不可用;暂不支持 macOS。",
|
||||
|
||||
"monitor.actions": "面板操作",
|
||||
"monitor.unavailable": "监控暂不可用,连接恢复后请重新开始。",
|
||||
"monitor.username": "用户名",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export const zhTWMonitorMessages = {
|
||||
"monitor.diskIo": "磁碟 I/O",
|
||||
"monitor.diskRead": "讀取",
|
||||
"monitor.diskWrite": "寫入",
|
||||
"monitor.diskIoHint": "Linux 底層區塊裝置的彙總 I/O,不重複統計分割區與疊加的虛擬裝置。取得兩次有效取樣前無法使用;暫不支援 macOS。",
|
||||
|
||||
"monitor.actions": "面板操作",
|
||||
"monitor.unavailable": "監控暫時無法使用,連線恢復後請重新開始。",
|
||||
"monitor.username": "使用者名稱",
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface ServerStats {
|
||||
swapTotal: number | null; // Total swap in MB
|
||||
swapUsed: number | null; // Used swap in MB
|
||||
topProcesses: ProcessInfo[]; // Top 10 processes by memory
|
||||
diskReadSpeed?: number | null; // Aggregate Linux hardware-backed block I/O, bytes/sec
|
||||
diskWriteSpeed?: number | null;
|
||||
diskPercent: number | null; // Disk usage percentage for root partition
|
||||
diskUsed: number | null; // Disk used in GB
|
||||
diskTotal: number | null; // Total disk in GB
|
||||
@@ -240,6 +242,8 @@ function normalizeServerStats(stats: Partial<ServerStats>): ServerStats {
|
||||
diskPercent: stats.diskPercent ?? null,
|
||||
diskUsed: stats.diskUsed ?? null,
|
||||
diskTotal: stats.diskTotal ?? null,
|
||||
diskReadSpeed: stats.diskReadSpeed ?? null,
|
||||
diskWriteSpeed: stats.diskWriteSpeed ?? null,
|
||||
disks: stats.disks || [],
|
||||
netRxSpeed: stats.netRxSpeed || 0,
|
||||
netTxSpeed: stats.netTxSpeed || 0,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { useI18n } from "../../application/i18n/I18nProvider";
|
||||
|
||||
function rate(value: number | null | undefined) {
|
||||
if (value == null || !Number.isFinite(value) || value < 0) return "--";
|
||||
if (value >= 1024 ** 2) return `${(value / 1024 ** 2).toFixed(1)} MiB/s`;
|
||||
if (value >= 1024) return `${(value / 1024).toFixed(1)} KiB/s`;
|
||||
return `${Math.round(value)} B/s`;
|
||||
}
|
||||
|
||||
/** Same shared-sampler rates in monitor cards, tables, and terminal overview. */
|
||||
export function DiskIoSummary({ stats }: {
|
||||
stats?: { diskReadSpeed?: number | null; diskWriteSpeed?: number | null } | null;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div data-disk-io className="flex flex-wrap gap-x-3 gap-y-0.5 text-[10px] tabular-nums text-muted-foreground" title={t("monitor.diskIoHint")}>
|
||||
<span>{t("monitor.diskIo")}</span>
|
||||
<span>{t("monitor.diskRead")} {rate(stats?.diskReadSpeed)}</span>
|
||||
<span>{t("monitor.diskWrite")} {rate(stats?.diskWriteSpeed)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { aggregateMountedDiskUsage } from '../../domain/systemDiskUsage';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useServerStats } from '../../application/state/useServerStats';
|
||||
import { ResourceBar } from './ResourceBar';
|
||||
import { DiskIoSummary } from './DiskIoSummary';
|
||||
import {
|
||||
SystemPanelEmpty,
|
||||
SystemPanelError,
|
||||
@@ -335,6 +336,8 @@ export const SystemOverviewTab = memo(function SystemOverviewTab({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3"><DiskIoSummary stats={stats} /></div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<InfoPill label={t('systemManager.overview.load')} value={formatLoad(stats.loadAverage)} />
|
||||
<InfoPill label={t('systemManager.overview.uptime')} value={formatDuration(stats.uptimeSeconds, t)} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { DiskIoSummary } from "../systemManager/DiskIoSummary";
|
||||
import type { MonitorStats } from "../../domain/monitorRuntime";
|
||||
import { ResourceBar } from "../systemManager/ResourceBar";
|
||||
import { useI18n } from "../../application/i18n/I18nProvider";
|
||||
@@ -15,7 +16,7 @@ export function MonitorDiskUsage({
|
||||
stats,
|
||||
limit,
|
||||
}: {
|
||||
stats?: (Pick<MonitorStats, "disks" | "diskPercent"> & Partial<Pick<MonitorStats, "diskUsed" | "diskTotal">>) | null;
|
||||
stats?: (Pick<MonitorStats, "disks" | "diskPercent"> & Partial<Pick<MonitorStats, "diskUsed" | "diskTotal" | "diskReadSpeed" | "diskWriteSpeed">>) | null;
|
||||
limit?: number;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
@@ -30,6 +31,7 @@ export function MonitorDiskUsage({
|
||||
return (
|
||||
<TooltipProvider delayDuration={150}>
|
||||
<div className={limit === 2 ? "min-h-[38px] space-y-1.5" : "space-y-1.5"}>
|
||||
<DiskIoSummary stats={stats} />
|
||||
{disks.map((disk, index) => (
|
||||
<Tooltip key={`${disk.mountPoint}-${index}`}>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -186,3 +186,19 @@ for (const view of ["list", "charts"] as const) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("disk I/O distinguishes warming up or unsupported from idle and reports both rates", () => {
|
||||
for (const [rates, expected] of [
|
||||
[{}, ["Read --", "Write --"]],
|
||||
[{ diskReadSpeed: null, diskWriteSpeed: null }, ["Read --", "Write --"]],
|
||||
[{ diskReadSpeed: 0, diskWriteSpeed: 0 }, ["Read 0 B/s", "Write 0 B/s"]],
|
||||
[{ diskReadSpeed: 1024, diskWriteSpeed: 2097152 }, ["Read 1.0 KiB/s", "Write 2.0 MiB/s"]],
|
||||
] as const) {
|
||||
const document = new JSDOM(renderToStaticMarkup(
|
||||
<I18nProvider locale="en"><MonitorDiskUsage stats={{ disks: [], diskPercent: null, ...rates }} /></I18nProvider>,
|
||||
)).window.document;
|
||||
const io = document.querySelector("[data-disk-io]")!;
|
||||
for (const text of expected) assert.ok(io.textContent?.includes(text));
|
||||
assert.match(io.getAttribute("title")!, /Linux.*not supported on macOS/);
|
||||
}
|
||||
});
|
||||
@@ -624,7 +624,7 @@ export function MonitorPanels({
|
||||
<SelectValue>{t("monitor.seconds", { count: panel.interval })}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MONITOR_INTERVALS.filter((v) => v >= 3).map((v) => (
|
||||
{MONITOR_INTERVALS.map((v) => (
|
||||
<SelectItem key={v} value={String(v)}>
|
||||
{t("monitor.seconds", { count: v })}
|
||||
</SelectItem>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
|
||||
// Read whole hardware-backed block devices only. Partitions live below these
|
||||
// entries; dm/md/loop/zram layers have no device link and would double-count I/O.
|
||||
// Linux block-stat sectors are always 512 bytes, regardless of device sector size.
|
||||
// No iostat dependency, extra exec, sampling sleep, or privileged access is needed.
|
||||
const DISK_IO_ASSIGNMENT = String.raw`diskio=$(if [ -r /proc/uptime ]; then awk '{printf "%s;",$1}' /proc/uptime; for d in /sys/block/*; do [ -e "$d/device" ] && [ -r "$d/stat" ] || continue; awk -v path="$d" 'NF>=7 {n=split(path,a,"/");printf "%s:%s:%s,",a[n],$3,$7}' "$d/stat"; done; fi)`;
|
||||
|
||||
function parseDiskIo(text) {
|
||||
if (typeof text !== "string") return null;
|
||||
const [time, counters, ...extra] = text.trim().split(";");
|
||||
const timestamp = Number(time);
|
||||
if (!time || extra.length || !Number.isFinite(timestamp) || timestamp < 0 || !counters) return null;
|
||||
const devices = new Map();
|
||||
for (const entry of counters.replace(/,$/, "").split(",")) {
|
||||
const fields = entry.split(":");
|
||||
const [name, read, write] = fields;
|
||||
if (fields.length !== 3 || !/^[a-zA-Z0-9_.!-]+$/.test(name) || devices.has(name) || !/^\d+$/.test(read) || !/^\d+$/.test(write)) return null;
|
||||
const readSectors = Number(read);
|
||||
const writeSectors = Number(write);
|
||||
if (!Number.isSafeInteger(readSectors) || !Number.isSafeInteger(writeSectors)) return null;
|
||||
devices.set(name, { readSectors, writeSectors });
|
||||
}
|
||||
return { timestamp, devices };
|
||||
}
|
||||
|
||||
function diskIoRates(current, previous) {
|
||||
const unavailable = { diskReadSpeed: null, diskWriteSpeed: null };
|
||||
if (!current || !previous || current.devices.size !== previous.devices.size) return unavailable;
|
||||
const seconds = current.timestamp - previous.timestamp;
|
||||
if (!(seconds > 0)) return unavailable;
|
||||
let read = 0;
|
||||
let write = 0;
|
||||
for (const [name, counters] of current.devices) {
|
||||
const before = previous.devices.get(name);
|
||||
if (!before || counters.readSectors < before.readSectors || counters.writeSectors < before.writeSectors) return unavailable;
|
||||
read += (counters.readSectors - before.readSectors) * 512;
|
||||
write += (counters.writeSectors - before.writeSectors) * 512;
|
||||
}
|
||||
return { diskReadSpeed: read / seconds, diskWriteSpeed: write / seconds };
|
||||
}
|
||||
|
||||
module.exports = { DISK_IO_ASSIGNMENT, parseDiskIo, diskIoRates };
|
||||
@@ -0,0 +1,51 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = require("node:fs");
|
||||
const { tmpdir } = require("node:os");
|
||||
const { join } = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const { DISK_IO_ASSIGNMENT, parseDiskIo, diskIoRates } = require("./monitorDiskIo.cjs");
|
||||
const unknown = { diskReadSpeed: null, diskWriteSpeed: null };
|
||||
|
||||
test("disk rates use remote fractional sample time and fixed 512-byte sectors", () => {
|
||||
const before = parseDiskIo("100.25;sda:100:200,nvme0n1:50:100,");
|
||||
const after = parseDiskIo("101.75;nvme0n1:80:140,sda:130:220,");
|
||||
assert.deepEqual(diskIoRates(after, before), { diskReadSpeed: 20480, diskWriteSpeed: 20480 });
|
||||
assert.deepEqual(diskIoRates(parseDiskIo("102.75;nvme0n1:80:140,sda:130:220,"), after), { diskReadSpeed: 0, diskWriteSpeed: 0 });
|
||||
});
|
||||
|
||||
test("first sample, unavailable counters, reset, and topology changes never look idle", () => {
|
||||
const before = parseDiskIo("100;sda:100:200,");
|
||||
assert.deepEqual(diskIoRates(before, null), unknown);
|
||||
assert.deepEqual(diskIoRates(null, before), unknown);
|
||||
for (const text of ["100;sda:110:210", "99;sda:110:210", "101;sda:99:210", "101;sda:110:199", "101;sdb:110:210", "101;sda:110:210,sdb:0:0"]) {
|
||||
assert.deepEqual(diskIoRates(parseDiskIo(text), before), unknown, text);
|
||||
}
|
||||
for (const text of [undefined, "", "100;", "NaN;sda:1:2", "100;sda:-1:2", "100;sda:1:NaN", "100;sda:1:2,sda:1:2", "100;sda:9007199254740992:1", "100;sda:1:2:3", "100;sda:1:2;extra"]) {
|
||||
assert.equal(parseDiskIo(text), null, String(text));
|
||||
}
|
||||
});
|
||||
|
||||
test("Linux collection includes whole hardware devices, excludes partitions and stacked devices", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "disk-io-"));
|
||||
try {
|
||||
const block = join(dir, "block");
|
||||
mkdirSync(block);
|
||||
writeFileSync(join(dir, "uptime"), "123.45 100.00\n");
|
||||
for (const name of ["sda", "nvme0n1", "dm-0", "md0", "loop0", "zram0"]) {
|
||||
mkdirSync(join(block, name));
|
||||
writeFileSync(join(block, name, "stat"), "1 2 100 4 5 6 200 8 9 10 11\n");
|
||||
if (["sda", "nvme0n1"].includes(name)) mkdirSync(join(block, name, "device"));
|
||||
}
|
||||
mkdirSync(join(block, "sda", "sda1"));
|
||||
writeFileSync(join(block, "sda", "sda1", "stat"), "1 2 100 4 5 6 200\n");
|
||||
const command = DISK_IO_ASSIGNMENT.replaceAll("/proc/uptime", join(dir, "uptime")).replaceAll("/sys/block", block);
|
||||
const output = execFileSync("sh", ["-c", `${command}; printf '%s' "$diskio"`], { encoding: "utf8" });
|
||||
const parsed = parseDiskIo(output);
|
||||
assert.equal(parsed.timestamp, 123.45);
|
||||
assert.deepEqual([...parsed.devices.keys()].sort(), ["nvme0n1", "sda"]);
|
||||
assert.deepEqual(parsed.devices.get("sda"), { readSectors: 100, writeSectors: 200 });
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-undef */
|
||||
const { DISK_IO_ASSIGNMENT, parseDiskIo, diskIoRates } = require("./monitorDiskIo.cjs");
|
||||
const { sampleSharedStats } = require("./sharedStats.cjs");
|
||||
const { GPU_ASSIGNMENT, parseGpuStats, withCachedMonitorGpu, cacheFullGpuStats, startMonitorGpuProbe } = require("./monitorGpuStats.cjs");
|
||||
const { executeBoundedSshCommand } = require("../boundedSshExec.cjs");
|
||||
@@ -1327,8 +1328,9 @@ function createSessionOpsApi(ctx) {
|
||||
// polling. Hosts without timeout also omit this optional metric.
|
||||
// macOS is skipped: powermetrics needs root, which stats must not.
|
||||
GPU_ASSIGNMENT,
|
||||
DISK_IO_ASSIGNMENT,
|
||||
// Output all stats (using CPURAW and PERCORERAW instead of CPU and PERCORE)
|
||||
`echo "CPURAW:$cpuraw|CORES:$cores|PERCORERAW:$percoreraw|MEMINFO:$meminfo|PROCS:$procs|NET:$net|GPU:$gpustat|HOST:$hostname_value|OS:$osname|KERNEL:$kernel|UPTIME:$uptime|LOAD:$loadavg"`
|
||||
`echo "CPURAW:$cpuraw|CORES:$cores|PERCORERAW:$percoreraw|MEMINFO:$meminfo|PROCS:$procs|NET:$net|GPU:$gpustat|DISKIO:$diskio|HOST:$hostname_value|OS:$osname|KERNEL:$kernel|UPTIME:$uptime|LOAD:$loadavg"`
|
||||
].filter(command => !session._monitorStats || (!command.startsWith('procs=') && !command.startsWith('gpustat='))).join('; ');
|
||||
|
||||
// Client-side run bound shared by the stats and disk probes; the remote
|
||||
@@ -1457,6 +1459,7 @@ function createSessionOpsApi(ctx) {
|
||||
let swapUsed = null;
|
||||
let topProcesses = []; // Array of { pid, memPercent, command }
|
||||
let disks = []; // Array of { mountPoint, used, total, percent }
|
||||
let diskIo = null;
|
||||
let networkInterfaces = []; // Array of { name, rxBytes, txBytes }
|
||||
let hostname = "";
|
||||
let osName = "";
|
||||
@@ -1473,6 +1476,8 @@ function createSessionOpsApi(ctx) {
|
||||
// macOS: command reports normalized CPU% directly (no delta needed)
|
||||
const val = parseFloat(part.substring(4).trim());
|
||||
if (!isNaN(val)) cpuDirect = Math.min(100, Math.max(0, Math.round(val)));
|
||||
} else if (part.startsWith('DISKIO:')) {
|
||||
diskIo = parseDiskIo(part.substring(7));
|
||||
} else if (part.startsWith('CPURAW:')) {
|
||||
const rawParts = part.substring(7).trim().split(/\s+/);
|
||||
if (rawParts.length >= 2) {
|
||||
@@ -1619,6 +1624,9 @@ function createSessionOpsApi(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
const diskRates = diskIoRates(diskIo, session.prevDiskIo);
|
||||
session.prevDiskIo = diskIo;
|
||||
|
||||
// Calculate network speed based on previous reading
|
||||
const now = Date.now();
|
||||
const prevNet = session.prevNetStats || { interfaces: [], timestamp: 0 };
|
||||
@@ -1761,6 +1769,7 @@ function createSessionOpsApi(ctx) {
|
||||
diskUsed, // Disk used in GB for root partition (backward compat)
|
||||
diskTotal, // Total disk in GB for root partition (backward compat)
|
||||
disks, // Array of all mounted disks
|
||||
...diskRates, // Aggregate physical-device I/O; null until two valid Linux samples
|
||||
networkReady: prevNet.timestamp > 0 && prevNet.interfaces.length > 0,
|
||||
netRxSpeed, // Total network receive speed (bytes/sec)
|
||||
netTxSpeed, // Total network transmit speed (bytes/sec)
|
||||
|
||||
@@ -1530,3 +1530,33 @@ test("monitor CPU samples return while an optional GPU driver is still blocked",
|
||||
await pending;
|
||||
}
|
||||
});
|
||||
|
||||
test("shared server stats parse Linux disk I/O across samples and leave macOS unavailable", async (t) => {
|
||||
let now = 100000;
|
||||
t.mock.method(Date, "now", () => now);
|
||||
let payload = `${LINUX_STATS}|DISKIO:10.5;sda:100:200,`;
|
||||
const conn = { exec(command, cb) { cb(null, fakeStream(selectServerStatsFixtureOutput(command, payload))); } };
|
||||
const session = { conn, type: "ssh", _monitorStats: true, _monitorIntervalMs: 1000 };
|
||||
const api = makeSessionOps(new Map([["diskio", session]]));
|
||||
const get = () => api.getServerStats({ sender: {} }, { sessionId: "diskio" });
|
||||
let result = await get();
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.stats.diskReadSpeed, null);
|
||||
assert.equal(result.stats.diskWriteSpeed, null);
|
||||
now += 1000;
|
||||
payload = `${LINUX_STATS}|DISKIO:11.5;sda:120:240,`;
|
||||
result = await get();
|
||||
assert.equal(result.stats.diskReadSpeed, 10240);
|
||||
assert.equal(result.stats.diskWriteSpeed, 20480);
|
||||
now += 1000;
|
||||
payload = `${LINUX_STATS}|DISKIO:12.5;sda:120:240,`;
|
||||
result = await get();
|
||||
assert.equal(result.stats.diskReadSpeed, 0);
|
||||
assert.equal(result.stats.diskWriteSpeed, 0);
|
||||
now += 1000;
|
||||
payload = MACOS_STATS;
|
||||
result = await get();
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.stats.diskReadSpeed, null);
|
||||
assert.equal(result.stats.diskWriteSpeed, null);
|
||||
});
|
||||
@@ -83,6 +83,7 @@ async function sampleSharedStats(session, collect, now = Date.now()) {
|
||||
const companion = session.moshStatsConn || session.etStatsConn;
|
||||
if (companion && result.success) {
|
||||
const state = stateFor(companion);
|
||||
state.context.prevDiskIo = session.prevDiskIo;
|
||||
state.context.prevNetStats = session.prevNetStats;
|
||||
state.context.prevCpuStats = session.prevCpuStats;
|
||||
state.history.latest = result;
|
||||
|
||||
@@ -93,3 +93,35 @@ test('a snapshot read before sampling still joins the existing endpoint history'
|
||||
assert.equal(readSharedStats(monitor).points, readSharedStats(terminal).points);
|
||||
assert.equal(readSharedStats(monitor).result.stats.cpu, 42);
|
||||
});
|
||||
|
||||
test('one-second requests never overlap and keep disk baselines on the physical transport', async () => {
|
||||
resetSharedStatsForTests();
|
||||
const conn = {};
|
||||
let release;
|
||||
let calls = 0;
|
||||
const contexts = [];
|
||||
const collect = context => {
|
||||
calls++;
|
||||
contexts.push(context);
|
||||
const previous = context.prevDiskIo;
|
||||
context.prevDiskIo = { timestamp: calls };
|
||||
return new Promise(resolve => { release = () => resolve({ success: true, stats: { diskReadSpeed: previous ? 512 : null, diskWriteSpeed: previous ? 1024 : null } }); });
|
||||
};
|
||||
const session = { conn, _monitorStats: true, _monitorIntervalMs: 1000 };
|
||||
const first = sampleSharedStats(session, collect, 1000);
|
||||
const overlapping = sampleSharedStats(session, collect, 2000);
|
||||
assert.equal(calls, 1);
|
||||
release();
|
||||
const [a, b] = await Promise.all([first, overlapping]);
|
||||
assert.equal(a, b);
|
||||
assert.equal(a.stats.diskReadSpeed, null);
|
||||
const next = sampleSharedStats(session, collect, Date.now() + 1001);
|
||||
assert.equal(calls, 2);
|
||||
release();
|
||||
assert.equal((await next).stats.diskReadSpeed, 512);
|
||||
const reconnect = sampleSharedStats({ ...session, conn: {} }, collect, Date.now() + 2002);
|
||||
assert.equal(calls, 3);
|
||||
assert.notEqual(contexts[0], contexts[2]);
|
||||
release();
|
||||
assert.equal((await reconnect).stats.diskReadSpeed, null);
|
||||
});
|
||||
@@ -119,6 +119,8 @@ function seed(
|
||||
diskPercent: 35,
|
||||
diskUsed: 35,
|
||||
diskTotal: 100,
|
||||
diskReadSpeed: i === 1 ? null : i === 2 ? 0 : 2 * 1024 ** 2,
|
||||
diskWriteSpeed: i === 1 ? null : i === 2 ? 0 : 512 * 1024,
|
||||
disks: Array.from({ length: i === 2 ? 10 : 2 }, (_, j) => ({
|
||||
mountPoint: j ? `/data-${j}` : "/",
|
||||
used: 35,
|
||||
|
||||
@@ -59,6 +59,7 @@ async function main() {
|
||||
() => run('document.querySelectorAll("[data-monitor-host]").length > 0'),
|
||||
"monitoring renders",
|
||||
);
|
||||
assert.match(await run('document.querySelector("[data-disk-io]").textContent'), /2.0 MiB\/s/);
|
||||
const screenshot = async (name) => {
|
||||
owner.webContents.sendInputEvent({ type: "mouseMove", x: 1, y: 1 });
|
||||
await wait(200);
|
||||
|
||||
+2
@@ -321,6 +321,8 @@ declare global {
|
||||
memPercent: number;
|
||||
command: string;
|
||||
}>;
|
||||
diskReadSpeed?: number | null; // Linux hardware-backed block devices, bytes/sec; null while unavailable/warming up
|
||||
diskWriteSpeed?: number | null; // Aggregate without partitions or stacked virtual devices
|
||||
diskPercent: number | null; // Disk usage percentage for root partition
|
||||
diskUsed: number | null; // Disk used in GB
|
||||
diskTotal: number | null; // Total disk in GB
|
||||
|
||||
Reference in new issue
Block a user