weihaobin0818-lgtm/dsh-system-monitor

dsh-system-monitor

DSH 侧栏实时硬件监控:CPU / GPU / 内存 / 磁盘一页看完,每秒刷新,每个读数都能下钻到是哪个应用。支持容量与内存耗尽预测、阈值告警与 NDJSON 导出。纯 Node,零运行时依赖,完整功能仅 Windows。

未分類cpudeepseekdeepseek-harness

dsh-system-monitor

DSH 侧栏实时硬件监控 —— CPU、GPU、内存、磁盘一页看完,每个读数都能下钻到具体应用。

独立的社区开源插件,与深度求索不存在隶属、合作、授权或背书关系。


在 DSH 侧栏放置实时硬件监控面板:CPU、GPU、内存、磁盘信息集中在同一页面展示,每秒自动刷新;每项指标均可下钻查看占用来源,附带对应应用原生图标,展示各应用资源占用明细,支持数据导出为 CSV、JSON、NDJSON 格式。

它和任务管理器的区别

任务管理器仅展示瞬时 CPU 占用(例如 80%),无法定位资源占用主体、预估负载持续时长、预判资源耗尽时间。 本插件为 DSH 侧栏插件,无需启动独立程序,在侧栏提供可视化硬件监控面板,同步完成这三项分析。

主要功能

一页看完

CPU、GPU、内存、磁盘四个环形仪表,加趋势图、核心热力图和磁盘列表,每秒刷新一次。支持舒适与紧凑两种密度,小屏也能一屏看完。

按应用归因

点任意读数进入明细,看到的不是一堆 PID,而是按应用分组的列表 —— 带应用自己的图标。可展开到单个进程,也可按占用、按增长、按磁盘三种方式排序。

预测

基于实测斜率预测磁盘写满时间与内存耗尽时间 —— 不是线性外推,会考虑采样窗口内的实际变化速率;斜率不够显著时宁可不报。

告警

超过阈值时弹出提示、写入告警历史、侧栏图标显示徽标。阈值可配,告警需持续一段时间才触发,避免瞬时抖动误报。

导出

CSV / JSON / NDJSON 三种格式,走系统「另存为」对话框,可自选文件夹、改文件名。NDJSON 是推荐的记录格式:追加不重写旧行,加字段不让旧数据失效。

落盘历史

滚动写 NDJSON 到磁盘,每天一个文件,默认保留 7 天、上限一年。30 秒批量落盘 —— 直写是每天 8.6 万次文件追加,对没人盯着的监控太浪费。

功能清单:

  • 概览 —— 三个环形仪表(CPU / 内存 / GPU)+ 趋势图 + 核心热力图 + 磁盘列表
  • 按应用归因 —— 点任意读数进入明细,看是哪些进程、占多少、可展开到单个进程
  • 预测 —— 磁盘写满时间、内存耗尽时间,基于实测斜率而不是线性外推
  • 告警 —— 超过阈值时弹提示、写入告警历史、侧栏图标显示徽标
  • 导出 —— CSV / JSON / NDJSON,走系统「另存为」对话框,可自选目录和文件名
  • 历史 —— 滚动写 NDJSON 到磁盘,默认保留 7 天,上限一年
  • 自定义时间范围 —— 预设 1 / 5 / 30 分钟,也可输入任意分钟数(最多 3 小时,即内存保留上限)

平台要求

系统Windows 10 / 11 —— 完整功能需要
DSH带 Web 界面的 profile(dsh web)
Node20 或更高(Harness 运行时已满足)
权限不需要管理员,全部读取当前用户自己会话内的数据

Linux 和 macOS 上仍可安装运行,但会降级为 Node 原生读数:CPU、内存和根卷。 GPU、按进程归因、图标、磁盘吞吐都会报告为不可用 —— 对应的面板会隐藏而不是显示为空。

安装

dsh plugin install dsh-system-monitor

或者开发时用 link: 指向本地目录,详见下方 Installing it。

不装驱动、不常驻服务、不联网,也不需要管理员权限。

质量

这个项目自带测试,而且测试被验证过会失败:

断言197 条,全部通过
变异测试15 个,全部被捕获
运行时依赖0 个 —— 只用 Node 和系统自带的 PowerShell

变异测试的含义是:故意往代码里注入 15 个 bug,如果测试没抓住任何一个,说明那条测试是假的。能全抓住,才说明这些断言真的会失败。

npm test              # 197 条断言
npm run test:mutate   # 15 个变异,验证测试本身
npm run release:check # 发布前检查:协议、版权、包内容、隐私信息

关于本文档

以下正文为英文。中文只覆盖到这里 —— 接口、配置项和实现细节请往下读。 配置项的含义在 Configuration 一节,每个都有说明。


English

A DSH bundle that puts a live hardware monitor in the Harness sidebar: CPU, GPU, memory and disk on one page, refreshed once a second, with a per-application breakdown — including each application's own icon — behind every gauge, supporting data export in CSV, JSON and NDJSON formats.

A community plugin. Not affiliated with, endorsed by, or connected to DeepSeek.

Requirements

PlatformWindows 10 / 11 for the full feature set
DSHa profile with the web surface (dsh web)
Node20 or newer — the Harness runtime already qualifies
Privilegesnone; everything is read from the current user's own session

On Linux and macOS the bundle still installs and runs, but degrades to the Node-native readings only: CPU, memory and the root volume. GPU, per-process attribution, icons, disk throughput and battery all report as unavailable, and their panels are hidden rather than shown empty.

When the two halves disagree

The Client is rebuilt and reserved every time the page loads. The Host module is not — it lives in the DSH process until DSH is restarted. So during development the two drift apart constantly, and a mismatch used to present itself as whatever it happened to break: a panel that vanished, an export that blanked the window, data that was quietly empty. Nothing pointed at the version.

So the Host declares a protocol number, and the Client refuses to pretend:

SAMPLER_PROTOCOL in metrics.jsThe wire contract. Bumped only when the payload's shape changes incompatibly — a renamed field, a changed unit, a removed key. Not for a bug fix.
REQUIRED_PROTOCOL in client.jsWhat this Client was written against.
MismatchA banner at the top of the panel naming both numbers, saying to restart DSH.

A Host reporting no protocol at all is treated as protocol 0, not as compatible — reading a missing field as "fine" would silence the warning in exactly the case it exists for.

When the collector itself fails

A sampler that cannot start is not a resource reading, but it is the most important thing this plugin can report: every counter-backed panel loses its data when it happens. It rides the existing alert channel rather than a second mechanism of its own, so the log line, the toast and the sidebar badge all pick it up for free.

It is announced once, not every tick — a repeating alert is noise, and noise is how a real alert gets ignored. It carries a code and no value, and the renderers leave the unit off rather than printing sampler null%. A sample arriving clears it.

Per-application disk throughput

Ranked separately from CPU, memory and GPU, and sampled on its own slower clock — a PerformanceCounter per process name is not free, and disk attribution changes on a scale of minutes. Measured at about 11 ms for eight names.

The reading is the counter's RawValue, which is cumulative bytes, differenced in Node. This matters: IO Read Bytes/sec is a rate, and a rate sampled once every thirty seconds describes the one second it was sampled in, not the thirty. Cumulative counters are correct at any cadence — the same lesson the network source taught before it was removed.

A counter that goes backwards (a recycled PID, a restarted process) yields no rate rather than negative traffic, and exited processes are dropped so the baseline map does not grow without bound over a long session.

Security model, stated plainly

This plugin is for a single-user machine and its routes are not authenticated.

Its four HTTP endpoints bind to loopback, and anything that can reach 127.0.0.1 on this machine can read them. What they expose is not trivial: the name and full executable path of every running process, the list of volumes, and the machine's hostname. On a shared or multi-user host that is an information disclosure, and no amount of care inside the plugin changes it.

The choice was deliberate rather than an oversight. An authenticated channel would mean a token the user has to manage, or an RPC surface that ties the plugin to one Harness transport — costs paid by every user to defend against a threat that only exists on a machine they share. So the honest thing is to say what this is, and let the reader decide:

  • Single-user machine — fine as it is.
  • Shared machine, terminal server, or anything with other logged-in users — do not run this. Loopback is not a security boundary between users on the same host.

Nothing is written outside ~/.dsh-system-monitor/, nothing is sent anywhere, and no elevated privileges are requested.

What it adds

WhereWhat
sidebar.panellist (id system-monitor)The gauge icon in the left rail; its label is the panel title and the accessible name
main (key system-monitor)The page that icon opens

The overview shows ring gauges with 60-second sparklines for CPU, memory and GPU, a per-logical-core column chart, and a used/free bar per volume. Colour follows the theme: brand below 70 %, warn from 70 %, error from 90 %.

Each card answers more than "how much":

  • CPU reports the real clock speed, derived from the turbo ratio the processor is actually running at, next to the nominal one.
  • Memory is broken out of its single percentage: free, standby cache, and the committed/limit pair. That last one is the useful one on Windows — a machine can read 80 % physical while the commit charge has already passed RAM and the page file is doing the work.
  • GPU carries the per-engine-type breakdown, shown on its detail tab.

A badge on the sidebar icon turns amber, then red, when a resource stays above its threshold — so a problem is visible without opening the panel at all.

Visualisations

ElementWhat it shows
Ring gaugesCurrent CPU, memory and GPU, each with a 60-second sparkline and a ▲/▼ against ten seconds ago
Trend chartCPU, memory and GPU on one shared timeline. Hovering locks a crosshair and reports all three series at that moment, which is what makes a spike attributable
Per-core columnsInstant load of every logical core
Core heatmapThe same cores across 40 seconds, newest column at the right — a single pegged core stands out here in a way a bar chart misses
Memory compositionA stacked bar of the top six applications by working set, then everything else and free memory, with an icon legend (memory tab)
Disk throughputRead and write history on one shared scale, above the per-volume and per-physical-disk rows (disk tab)
GPU enginesBusiest engine first — 3d, copy, video decode, compute (GPU tab)
Application tablesPer-application bars, scaled to the largest row shown

Chart colours (the heat ramp and the composition palette) are deliberately not theme tokens: a heat ramp needs fixed hues per severity bucket, and a categorical palette needs evenly separated hues. Both use one fixed lightness and saturation so they stay legible in light and dark themes. Everything else is --dsw-alias-*.

Look and feel

The page follows the Harness design language rather than inventing one: flat cards with a .5px hairline border on --dsw-alias-bg-layer-1, --dsw-radius-xl corners, a 20-21px/600 page title over 14px/600 card titles, tabular-nums on every figure, and focus rings built from --dsw-focus-ring-*. Depth comes from layered backgrounds (layer-1 card, layer-2 detail surface, layer-3 wells and tracks) instead of drop shadows, which is how the shipped pages do it.

Each gauge card carries a faint radial tint and a resource glyph in its own state colour, so the three cards are distinguishable before any text is read. The live indicator pulses only while readings are arriving, and every transition is disabled under prefers-reduced-motion.

The stylesheet uses 27 --dsw-* tokens, all of them ones the shipped Harness pages already reference — no token name is guessed, so a rename degrades appearance rather than breaking rendering.

Clicking a gauge — or its title button — opens 占用明细 / Usage details for that resource, which answers "which application is using it?":

TabRows
CPUApplications ranked by CPU share of the whole machine, with memory beside
内存Memory composition bar, then applications ranked by working set with CPU beside
GPUEngine-type breakdown, then applications ranked by GPU utilisation
磁盘Throughput history, volumes with used/total/free, then per-physical-disk rates

The three application tabs also carry a 按占用 / 按增长 switch: usage ranks by the tab's resource, growth ranks by memory movement over the last few minutes. Selecting a row expands it into that application's own processes.

Rows are grouped by application, so twelve msedge processes appear once with a process count and one icon. Bars scale to the largest row shown; the exact value is always in the number column.

Attribution

The detail tables answer "who is using it". Two things go further, because absolute numbers hide the two cases people actually care about.

Growth ranking. The Host captures a per-application memory baseline every 30 s and keeps ten minutes of them. The 按增长 / By growth switch ranks applications by memory movement over the longest window available, so a process leaking slowly rises to the top instead of hiding underneath whatever happens to be largest right now. Applications that did not exist in the baseline are tagged as new rather than reported as growth, movement under 8 MB is treated as noise, and the row states the window it actually measured. In a 62-second observed window the largest movers were a chat client at +42.7 MB, a shell at +41.2 MB, and the Harness itself at −191.0 MB — the last one being memory it had just released, which no absolute reading would ever show.

Single application detail. Selecting any row expands it to that application's own processes — PID, CPU, memory, GPU and start time — plus the executable path it was resolved from. This is where "which of Chrome's nine processes is the problem" gets answered, and it is why the tables group by application in the first place.

Start times come from Process.StartTime, which throws for protected processes; about 141 of 260 processes on the development machine expose one, and the rest show a dash.

History, export and persistence

Time range. The Host keeps thirty minutes of aligned samples and the header offers 1 / 5 / 30 分钟. Whatever the window, the payload is reduced to 180 points, so a thirty-minute view costs the same ~9 KB as a one-minute one, and an out-of-range request is clamped to the nearest offered window rather than trusted.

Session summary. Extremes since the peaks were first accumulated — CPU, memory, GPU and disk latency — each with the moment it happened and which application was to blame at that time, plus the running averages. A live reading answers "what now"; this answers "how bad has it been".

Export. GET …/export?format=csv|json|ndjson&scope=series|apps|full&window=… answers three different questions, each with the formats that suit it:

ScopeWhat it isFormats
seriesOne row per sample: CPU, memory, GPU, disk read/write, latency, queueCSV · JSON · NDJSON
appsPer application now: processes, CPU, memory, GPU, growth, executable pathCSV · NDJSON
fullOne bundle with every reading, capability, the config, alert history and session peaksJSON

All carry content-disposition: attachment, so the Status tab links to them directly. Series sampled at different rates leave gaps in each other's rows rather than being resampled into numbers that were never measured.

apps is point-in-time, not a window average, because per-application history is not retained — only the current aggregate and the memory-growth baseline. The growth columns are the exception: they describe a real span and carry the span they measured.

Why NDJSON is the format to standardise on

format=ndjson writes one complete JSON object per line. It is the format a sampler would log to, and it beats CSV on the three properties that matter for a log that grows while you watch it:

CSVNDJSON
AppendSafe, but the header defines the schemaSafe, and each line is self-describing
Adding a fieldInvalidates every existing file — old rows now have the wrong shapeOld rows stay valid; they simply lack the field
Torn writeA partial flush corrupts the row, and often the parse of everything after itCosts exactly one line
NestingImpossible — the app's executable path is fine, a nested object is notNative
Reading itEvery spreadsheetjq, pandas.read_json(lines=True), DuckDB read_ndjson_auto()

The last row is the practical argument. DuckDB turns a month of NDJSON into SQL or Parquet in one statement, with no import step:

-- query it directly
SELECT date_trunc('minute', to_timestamp(t / 1000)) AS minute, avg(cpu), max(mem)
FROM read_ndjson_auto('system-monitor-series-*.ndjson')
GROUP BY 1 ORDER BY 1;

-- or convert the archive to a columnar file
COPY (SELECT * FROM read_ndjson_auto('system-monitor-*.ndjson'))
TO 'monitor.parquet' (FORMAT PARQUET);

CSV remains the default for the same reason it always was: it is what people open in Excel. JSON remains for the case where one document is wanted rather than rows.

Long-term history. One NDJSON file per day under ~/.dsh-system-monitor/history/, appended in thirty-second batches — writing straight through at one sample a second would be 86,400 file appends a day for a monitor nobody is watching.

Retention is bounded on purpose. historyDays defaults to 7 and is clamped to a year, and the prune runs on every flush rather than on a schedule that might not survive a restart. A monitor that silently fills a disk is a worse bug than one with no history at all, so this is not configurable into a problem:

historyDaysEffect
7 (default)One week of per-second samples, about 60 MB
0 or negativeHistory off entirely; nothing is buffered, let alone written
99999Clamped to 365

At roughly 100 bytes a line, a day costs about 8 MB. The files are the same NDJSON the export writes, so a month of them is one DuckDB query.

Persistence. Extremes, the alert log and a downsampled chart tail are written to ~/.dsh-system-monitor/state.json — at most once a minute, and only when a peak or an alert actually changed. On the next start they are restored, so the plugin can still answer "has it been like this all week" after a restart:

startedAt : 2026-10-11T14:56:38Z   (this process)
since     : 2026-10-11T14:55:14Z   (peaks accumulated since — restored)
cpu peak  : 18.2%  at 14:55:52  label=DeepSeek Harness
series    : 35 points already there before the first sample

Set persist: false to disable it. Every read and write is wrapped: a read-only home directory is a valid environment, not an error, and a failed save is silently skipped rather than surfaced as a plugin fault.

Forecasts

Two things actually run out, so two things are projected: free disk space and commit charge. Both are sampled once a minute into an hour-long series, and both are fitted with a least-squares slope.

The design rule is never state a projection the data does not support:

  • A volume is only projected when its trend is genuinely downward, the baseline spans at least ten minutes, and the answer is under ten years.
  • Memory is only projected when the commit charge is genuinely rising and the baseline spans at least five minutes.
  • The raw slope and the measured window are always reported, so a flat trend reads as "measured, going nowhere" rather than looking broken. The card says 按最近 12 分钟的速度,容量基本不变 instead of showing nothing.
CaseResult
50 GiB free, losing exactly 1 GiB/day, 10 min windowdaysLeft: 50
Same volume, flatslope 0, daysLeft: null
Same volume, 4 min windowslope measured, daysLeft: null
Volume gaining spacedaysLeft: null — never projects a fill-up
Commit +1 GiB over 25 min, 9 GiB headroomsecondsLeft: 13500 (exact)
Commit shrinkingsecondsLeft: null

Battery comes from Get-CimInstance Win32_Battery, polled every twelfth sample because it is a slow query and battery state changes on a scale of minutes. It is probed inside the loop rather than at startup so a machine where CIM is unavailable cannot delay the first reading. Note that the Battery Status performance counter set does not exist on Windows at all — CIM is the only route — and EstimatedRunTime is reported as zero while charging, so implausible values are dropped rather than shown as "0 minutes left".

Dashboard layout

The panel is built for a wall display as much as for a laptop.

Density. 舒适 / Compact switches every size that matters — ring gauge 104→84 px, trend chart 168→108 px, per-core bars 60→36 px, plus tighter padding and card gaps. Compact fits the whole dashboard on a 1080p screen; the page still scrolls if it has to, so nothing can become unreachable. The choice is kept in localStorage and survives a reload.

Responsive columns. The page is capped at 1560 px, the three gauges reflow at 240–276 px, and the core grid and disk card sit in one auto-fit region: stacked on a narrow window, side by side on a wide one. Previously they were always stacked, which wasted most of a wide screen.

Trend chart. It is the centrepiece, so it carries more:

  • a time axis — clock times for long windows, −30s-style offsets for short ones, whichever the reader is actually asking for;
  • alert spans rather than single markers. A transition sets the level until the next one, and that interval is shaded, so you can see how long something stayed bad instead of only when it tripped;
  • a peak marker per series, so a spike that has already passed is still readable rather than looking like a quiet stretch.

Every one of these is measured in the render suite rather than assumed: the harness counts axis ticks, shaded spans and peak dots, and asserts the ring, chart and core-bar sizes for both densities.

Diagnostics

Four checks run on every sample and answer "is something actually wrong", not just "what are the numbers".

Resource pressure. Each resource is scored 0–1 against what counts as saturated for it — CPU 90 %, memory 95 %, GPU 95 %, disk 50 ms latency or a queue of 4 — and the worst one is named as the bottleneck in the header line. Capacity is deliberately excluded: a 90 % full disk is not a slow disk. The Status tab draws all four as bars, with the bottleneck highlighted.

CPU throttling. Live clock ÷ nominal, but only while the CPU is above 45 %. A low clock at idle is power management, not throttling, and flagging it would make the chip meaningless. Above the load floor, a ratio under 0.85 raises a chip on the CPU card and the session peak clock stays visible for comparison — on the development machine that is 2.0 GHz nominal against a 4.07 GHz peak.

Disk contention. Avg. Disk sec/Transfer and Current Disk Queue Length sit alongside throughput on the disk card, in the disk tab and in a latency history. Throughput alone cannot distinguish a busy disk from an idle one moving the same bytes; latency and queue depth can.

Alert history. Every alert-level transition is recorded with its reasons, so "when did this start" has an answer. Transitions appear as vertical markers on the trend chart, aligned with whatever the chart was doing at that moment, and as a list on the Status tab. Only changes are recorded, and the log is bounded, so a flapping resource cannot grow it without limit.

How the numbers are collected

Everything runs in the Host half, which keeps one sampling cadence regardless of how many pages are open.

  • CPU — os.cpus() time deltas per logical core, aggregated and per-core.
  • Memory — os.totalmem() / os.freemem().
  • Disk — fs.statfs over the mounted drive letters, re-scanned every 20 s.
  • GPU, clock speed, memory composition, processes, disk throughput and latency — one long-lived PowerShell child. It reads the WDDM performance counters `\G