LabWatch Lite — Project Report项目汇报

A lightweight self-hosted dashboard for monitoring NVIDIA GPUs and AI development servers.

轻量级、自托管的 NVIDIA GPU 与 AI 开发服务器监控面板。

v1.1.0 installable tool + validated on a real 8-GPU serverv1.1.0 可安装工具 + 已在真实 8 卡服务器验证 Single host · multiple GPUs单机 · 多卡 179 backend tests后端 179 个测试 77 frontend tests前端 77 个测试 8 browser E2E tests浏览器 E2E 8 个 Docker + GPU verifiedDocker + GPU 已验证 MIT

1. What was built

1. 构建内容

LabWatch answers, on one self-refreshing page, the questions that otherwise require an SSH session and repeated nvidia-smi, htop and df -h runs: which GPU is free, what is running on it, who owns those processes, how long they have been running, whether RAM or disk is about to run out, and what utilisation looked like over the last hour, six hours or day.

LabWatch 在一个自动刷新的页面上,回答了那些原本需要登录 SSH 并反复执行 nvidia-smihtopdf -h 才能回答的问题: 哪张卡是空的、上面在跑什么、那些进程属于谁、已经跑了多久、内存或磁盘是否即将耗尽, 以及过去一小时、六小时、一天里的利用率是什么样。

2 s
Live refresh interval实时刷新间隔
10 s
History write interval历史写入间隔
24 h
Default retention默认保留时长
87 %
Backend line coverage后端行覆盖率

Implemented features

已实现的功能

AreaDetail
GPU telemetryUtilisation, VRAM used/total and %, temperature, power draw, enforced power limit and derived %, fan, SM and memory clocks, persistence mode, process count.
GPU processesNVML compute PIDs joined with psutil: user, full command line, CPU %, resident memory, status, start time, runtime (human and clock form).
Host telemetryCPU usage, physical/logical cores, frequency, load average, per-core breakdown; RAM used/total/available/%; disk with the primary mount plus optional extra mounts; hostname, OS, kernel, platform, uptime.
HistoryHost and per-GPU samples persisted to SQLite (WAL), background collector, retention pruning plus a hard row cap, and 1H/6H/24H series endpoints.
DashboardHost tiles, one card per GPU, sortable/filterable/searchable process table, four history charts, sticky header with connection state, system/light/dark themes applied before first paint.
ResilienceMissing driver, missing GPU, unsupported sensors, vanishing processes and database faults all degrade locally instead of breaking the page or the API.
Demo modeDeterministic synthetic GPUs, processes and history, labelled Demo Data, so the project is demonstrable and testable without an NVIDIA card.
OperationsMulti-stage Docker image, Compose deployment with a named volume, environment-variable configuration, health endpoint, CI pipeline with a container smoke test.
领域细节
GPU 遥测利用率、显存已用/总量及百分比、温度、功耗、强制功耗上限及推导百分比、风扇、SM 与显存频率、持久化模式、进程数。
GPU 进程把 NVML 计算 PID 与 psutil 关联:用户、完整命令行、CPU %、常驻内存、状态、启动时间、运行时长(人类可读与时钟两种形式)。
主机遥测CPU 使用率、物理/逻辑核心数、频率、负载均值、单核明细;内存已用/总量/可用/百分比;磁盘主挂载点及可选的其他挂载点;主机名、系统、内核、平台、运行时长。
历史数据主机与逐卡采样写入 SQLite(WAL 模式),后台采集器、按保留窗口清理并设行数硬上限,提供 1H/6H/24H 序列接口。
面板主机卡片、每张 GPU 一张卡片、可排序/过滤/搜索的进程表、四张历史图表、带连接状态的吸顶顶栏、首屏渲染前即生效的系统/浅色/深色主题。
健壮性驱动缺失、无 GPU、传感器不支持、进程消失、数据库故障,全部局部降级,而不是让页面或 API 崩掉。
演示模式确定性的合成 GPU、进程与历史数据,明确标注 Demo Data,因此在没有 N 卡的机器上也能演示与测试。
运维多阶段 Docker 镜像、带命名卷的 Compose 部署、环境变量配置、健康检查接口、含容器冒烟测试的 CI 流水线。

2. Architecture

2. 架构

Architecture diagram / 架构图
One host, one container, three data sources: NVML, psutil and SQLite.
单机、单容器、三个数据源:NVML、psutil 与 SQLite。

The browser polls a single aggregated endpoint (/api/overview) so one refresh is one round trip. Polling pauses while the tab is hidden. A background asyncio task samples the collectors every ten seconds in a worker thread (NVML and psutil are blocking) and writes to SQLite. NVML is initialised lazily, exactly once, behind a lock, because the library is not thread safe.

浏览器只轮询一个聚合接口(/api/overview),因此一次刷新只有一次往返。标签页 隐藏时暂停轮询。后台 asyncio 任务每十秒在工作线程中采集一次(NVML 与 psutil 都是阻塞 调用)并写入 SQLite。NVML 采用惰性初始化,且因为该库并非线程安全,全过程只初始化一次 并加锁保护。

v1 deliberately has no WebSockets, no message queue, no cache layer and no authentication: at a two second refresh they would add operational surface without changing the experience.

v1 刻意不引入 WebSocket、消息队列、缓存层与认证:在 2 秒刷新频率下,它们只会增加运维 面,并不会改善使用体验。

3. Screenshots

3. 截图

Captures below come from the production container image. The first two are demo mode; the third is the same image reading a real RTX 4060 through NVML.

以下截图全部来自生产容器镜像。前两张为演示模式;第三张是同一个镜像通过 NVML 读取真实 RTX 4060 的效果。

LabWatch dashboard / LabWatch 面板
Demo data: host tiles, one card per GPU, the GPU process table and the start of the history section. The Demo Data chip is always visible so synthetic values cannot be mistaken for real ones.
演示数据:主机卡片、每卡一张的 GPU 卡片、GPU 进程表以及历史区域的起始部分。界面始终显示 Demo Data 标记,避免合成数据被误认为真实数据。
GPU cards / GPU 卡片
GPU cards: the four headline metrics are readable at a glance; secondary stats are quieter.
GPU 卡片:四项核心指标一眼可读,次要指标在视觉上更克制。
LabWatch reading a real RTX 4060 / LabWatch 读取真实 RTX 4060
Real hardware, no demo data: the RTX 4060 on this development host, and the empty state rendered when nothing holds GPU memory.
真实硬件、非演示数据:本开发机上的 RTX 4060,以及无进程占用显存时渲染出的空状态。
GPU process table / GPU 进程表
Process table with search, per-GPU filter chips and click-to-sort columns.
进程表支持搜索、按 GPU 过滤以及点击表头排序。
History charts / 历史图表
History: host utilisation, per-GPU utilisation, VRAM as a percentage of capacity, and temperature.
历史曲线:主机利用率、逐卡利用率、以容量百分比表示的显存占用,以及温度。
Light theme / 浅色主题
Light theme, driven by the same design tokens.
浅色主题,由同一套设计令牌驱动。

4. Tests and results

4. 测试与结果

SuiteTestsResultNotes
Backend (pytest)179pass87 % line coverage over labwatch/; NVML replaced by a configurable fake. Verified stable over eight consecutive runs.
Frontend (Vitest)77passFormatting, theme, API client, host tiles, GPU cards, process table sorting/filtering/search, empty and no-GPU states.
End-to-end (Playwright)8passReal Chromium against a demo backend and the dev server; also asserts zero console errors.
Ruff lintcleanruff check app tests
TypeScriptcleantsc -b with strict, noUnusedLocals, noUnusedParameters.
Production buildpassvite build; ~633 kB JS / ~21 kB CSS before gzip.
Docker imagepassMulti-stage build, 302 MB, runs as uid 10001.
Container smoke testpassHealthy in 2 s; dashboard, assets, SPA fallback and every API endpoint verified inside the container.
Container with GPU passthroughpass--gpus all: NVML initialised in-container (driver 581.80) and reported the real device.
7 hour sustained runpass11541 writes, 0 collector failures, sample interval held at 2 s without drift.
套件数量结果说明
后端(pytest)179通过labwatch/ 行覆盖率 87%;用可配置的仿真实现替换 NVML。连续 8 次运行均稳定。
前端(Vitest)77通过格式化、主题、API 客户端、主机卡片、GPU 卡片、进程表排序/过滤/搜索、空状态与无 GPU 状态。
端到端(Playwright)8通过真实 Chromium 对接演示后端与开发服务器;同时断言零 console 错误。
Ruff 静态检查干净ruff check app tests
TypeScript干净tsc -b,开启 strictnoUnusedLocalsnoUnusedParameters
生产构建通过vite build;gzip 前约 633 kB JS / 21 kB CSS。
Docker 镜像通过多阶段构建,302 MB,以 uid 10001 运行。
容器冒烟测试通过2 秒内健康;在容器内验证面板、静态资源、SPA 回退路由以及全部 API 接口。
容器 + GPU 直通通过--gpus all:容器内成功初始化 NVML(驱动 581.80)并读到真实设备。
7 小时持续运行通过写入 11541 次,采集失败 0 次,采样间隔稳定保持 2 秒无漂移。

Collector failure modes explicitly covered

明确覆盖的采集器失败路径

Verification against real hardware

真实硬件验证

The development host has an NVIDIA GeForce RTX 4060 Laptop GPU (driver 581.80, CUDA driver 13.0). LabWatch's readings were compared against nvidia-smi both natively and from inside the container.

开发机配备 NVIDIA GeForce RTX 4060 Laptop GPU(驱动 581.80,CUDA 驱动 13.0)。LabWatch 的 读数在本机与容器内分别与 nvidia-smi 做过对比。

Metricnvidia-smiLabWatchVerdict
Utilisation9 % / 27 % / 40 %10 % / 28 % / 40 %within one sample
VRAM total8188 MiB8188 MiBexact
VRAM used1369 MiB1599 MiBwithin sample skew
Temperature58 °C / 48 °C / 71 °C57 °C / 47 °C / 71 °Cwithin one sample
Power draw3.95 W / 7.82 W4.0 W / 7.9 Wexact to 1 dp
Power limit[N/A]N/Acorrectly absent
指标nvidia-smiLabWatch判定
利用率9 % / 27 % / 40 %10 % / 28 % / 40 %单次采样误差内
显存总量8188 MiB8188 MiB完全一致
显存占用1369 MiB1599 MiB采样偏差内
温度58 °C / 48 °C / 71 °C57 °C / 47 °C / 71 °C单次采样误差内
功耗3.95 W / 7.82 W4.0 W / 7.9 W小数点后一位一致
功耗上限[N/A]N/A正确显示为不支持

The GPU process list matches nvidia-smi --query-compute-apps pid for pid, with additional user, command, CPU and runtime detail that nvidia-smi does not provide. History was left running and read back: 361 host points at a 10 second interval for the 1H window, and an 8641 point 24H series after a restart.

GPU 进程列表与 nvidia-smi --query-compute-apps 的 PID 完全一致,并额外提供 nvidia-smi 没有的用户、命令行、CPU 与运行时长。历史数据经长时间运行后回读: 1H 窗口在 10 秒间隔下得到 361 个主机采样点,重启后 24H 序列有 8641 个点。

5. Real Linux multi-GPU server validation

5. 真实 Linux 多卡服务器验证

Everything above was measured on the Windows development host. The project was then deployed to a real research server - eight GPUs under live experimental load - and re-verified end to end. This section records that validation, because "the API returns something" is not the same as "the numbers mean the right thing on a real Linux host".

以上数据均在 Windows 开发机上测得。随后项目被部署到一台真实的科研服务器上 —— 八张 GPU、正在跑实验 —— 并重新做了端到端验证。本节记录这次验证,因为"接口能返回数据" 和"数据在真实 Linux 主机上语义正确"是两回事。

Environment

服务器环境

ItemValue
DistributionUbuntu 22.04.5 LTS, kernel 6.8.0-124-generic
GPUs8 × NVIDIA GeForce RTX 4090 (49140 MiB each)
Driver / CUDA580.173.02 / CUDA driver 13.0
Host resources128 logical CPUs, 503.5 GB RAM
Filesystems/ 91.7 %, /nfs-data1 98.4 %, /nfs-data2, /nfs-data3, /nfs-data4, /boot/efi
Deployment modeNative Python (Docker is not installed on this host). Own venv in the project directory, created with --system-site-packages so it inherits the user's conda environment read-only. No system package, service, or existing environment was modified.
Concurrent loadAnother user's experiments were running throughout; they were never started, stopped or signalled by LabWatch.
项目
发行版Ubuntu 22.04.5 LTS,内核 6.8.0-124-generic
GPU8 × NVIDIA GeForce RTX 4090(每张 49140 MiB)
驱动 / CUDA580.173.02 / CUDA 驱动 13.0
主机资源128 逻辑核,503.5 GB 内存
文件系统/ 91.7 %、/nfs-data1 98.4 %、/nfs-data2/nfs-data3/nfs-data4/boot/efi
部署方式原生 Python(该主机未安装 Docker)。在项目目录下用 --system-site-packages 建立独立 venv,只读继承你的 conda 环境。未修改任何系统软件包、服务或既有环境。
并发负载全程有其他用户的实验在跑;LabWatch 从未启动、停止或向其发送任何信号。

Multi-GPU discovery and metric agreement

多卡发现与指标一致性

All eight devices were discovered automatically. LabWatch was compared against nvidia-smi captured in the same instant, on every GPU:

八张卡全部被自动发现。在每一张卡上,把 LabWatch 与同一瞬间采集的 nvidia-smi 逐项对比:

MetricAgreement over 8 GPUs
GPU utilisationexact (0 %, 98 %, 100 % cases all matched)
Memory totalexact - 49140 MiB on all eight
Temperatureexact (25-72 °C)
Power drawexact to 1 dp (13.4 W idle, 449.8 W loaded)
Power limitexact - 450.0 W
指标8 张卡的一致性
GPU 利用率完全一致(0 %、98 %、100 % 各种情况均吻合)
显存总量完全一致 —— 八张卡均为 49140 MiB
温度完全一致(25–72 °C)
功耗小数点后一位一致(空闲 13.4 W,满载 449.8 W)
功耗上限完全一致 —— 450.0 W

GPU process mapping on a shared, multi-user server

多人共享服务器上的 GPU 进程映射

LabWatch ran as one user and reported another user's CUDA processes - the normal case on a shared lab machine. Each entry was checked against the driver and against /proc:

LabWatch 以一个用户的身份运行,却要报告另一个用户的 CUDA 进程 —— 这是共享实验机的常态。 每一条记录都与驱动和 /proc 做了核对:

FieldResult
PID → GPU indexcorrect for all 8 processes; independently confirmed because each workload's JSONL filename encodes its GPU (e.g. ..._gpu5_... matched gpu_index=5)
GPU memory per process0 MiB difference against nvidia-smi --query-compute-apps
Usernamematched /proc/<pid> ownership, including other users (niuyizhuo, lipeilang)
Command linefull 1018-byte command line, including all arguments
Runtimematched /proc start time to the second
CPU %~100 % per process, consistent with ps reporting 107-110 %
Resident memory44.1 GB, consistent with ps reporting 8.7 % of 503.5 GB
字段结果
PID → GPU 索引8 个进程全部正确;并有独立佐证 —— 每个任务的 JSONL 文件名里编码了 GPU(如 ..._gpu5_... 对应 gpu_index=5
进程显存nvidia-smi --query-compute-apps 偏差 0 MiB
用户名/proc/<pid> 属主一致,包含其他用户(niuyizhuolipeilang
命令行完整 1018 字节命令行,含全部参数
运行时长/proc 启动时间精确到秒一致
CPU %每进程约 100 %,与 ps 的 107–110 % 相符
常驻内存44.1 GB,与 ps 报的 503.5 GB 的 8.7 % 相符
Cross-user visibility depends on the host's /proc mount options. This server mounts /proc without hidepid, so user and command resolution works for every process. On a host that sets hidepid=2 those two fields degrade to N/A while PID, GPU, VRAM and runtime keep working.
跨用户可见性取决于主机的 /proc 挂载参数。本服务器未启用 hidepid, 因此所有进程的用户名与命令行都能解析。若主机设置了 hidepid=2, 这两个字段会降级为 N/A,而 PID、GPU、显存与运行时长仍然可用。

Host metric semantics

主机指标语义

Deployment was native, so CPU, RAM and disk are the host's own. The dashboard was still checked against the commands an operator would run, and two real defects fell out (numbers 11 and 12 below).

本次为原生部署,因此 CPU、内存、磁盘都是宿主机的真实数据。即便如此,面板仍与运维人员会 使用的命令做了比对,并因此发现两个真实缺陷(见下方第 11、12 条)。

MetricReferenceLabWatch
CPU load averageuptime 12.31 / 12.88 / 14.8412.31 / 12.88 / 14.84
Memory usedfree 42.5 GB (8.4 %)42.5 GB (8.4 %) after the fix
Filesystemsdf 6 real mountsall 6, percentages matching df
API latency under 8-GPU loadtarget < 500 ms0.01-0.04 s
API errors / 5xxnone expectednone; collector_errors = 0
指标参照LabWatch
CPU 负载均值uptime 12.31 / 12.88 / 14.8412.31 / 12.88 / 14.84
内存已用free 42.5 GB(8.4 %)修复后为 42.5 GB(8.4 %)
文件系统df 6 个真实挂载点6 个全部显示,百分比与 df 一致
8 卡负载下 API 延迟目标 < 500 ms0.01–0.04 s
API 错误 / 5xx应为无无;collector_errors = 0
LabWatch on the 8-GPU lab server
The real research server: 8 × RTX 4090, live experimental load, and the filesystem panel flagging /nfs-data1 at 98.4 % in red - the defect that only a real server could reveal.
真实科研服务器:8 × RTX 4090、正在跑实验,文件系统面板把 98.4 % 的 /nfs-data1 标红 —— 这个缺陷只有真实服务器才能暴露。
Eight GPU cards
Eight GPU cards render cleanly with no overflow, mixing busy (100 %, 447 W, 69 °C) and idle (0 %, 15 W, 31 °C) devices.
八张 GPU 卡片排布干净、无溢出,繁忙(100 %、447 W、69 °C)与空闲(0 %、15 W、31 °C)设备对比清晰。
GPU process table on the lab server
Another user's eight CUDA processes, each mapped to its GPU, with username, full command line, CPU %, RAM and runtime.
其他用户的 8 个 CUDA 进程,逐个映射到对应 GPU,并给出用户名、完整命令行、CPU %、内存与运行时长。
History on the lab server
History from the real server: host utilisation, eight GPU utilisation traces, VRAM per GPU, and temperature.
真实服务器的历史曲线:主机利用率、8 条 GPU 利用率轨迹、逐卡显存占用与温度。

6. v1.1 — from a project to a tool

6. v1.1 —— 从"项目"变成"工具"

v1.0 answered "what is happening on my GPU server?" v1.1 answers a different question: "how do I open LabWatch without thinking about it?" The monitoring core was frozen; everything here is install, start, access and daily use.

v1.0 回答的是"我的 GPU 服务器上发生了什么"。v1.1 回答的是另一个问题: "我怎么能不假思索地打开 LabWatch"。核心监控功能全部冻结,这一轮只改进安装、 启动、访问与日常使用。

1
command to install安装命令条数
0
npm steps for users用户需执行的 npm 步骤
250 KB
wheel with dashboard含面板的 wheel 体积
87 %
Backend coverage后端覆盖率

Packaging

打包

CLI

命令行

labwatch                     # serve and open the dashboard
labwatch --demo              # synthetic GPUs, no hardware
labwatch doctor              # can this machine run LabWatch?
labwatch start --background  # detached
labwatch status              # one-line answer, or --json
labwatch stop / open / version
labwatch                     # 启动并打开面板
labwatch --demo              # 合成 GPU,无需硬件
labwatch doctor              # 这台机器能跑 LabWatch 吗
labwatch start --background  # 后台运行
labwatch status              # 一行答案,或 --json
labwatch stop / open / version

The CLI uses only the standard library, deliberately: --help and doctor must work even when a dependency failed to install. doctor checks Python, the dependencies, NVML, the GPUs, the port, the data directory and the bundled dashboard, and names the fix for each failure rather than printing a traceback. status --json is the interface the editor consumes.

CLI 刻意只使用标准库:即使某个依赖安装失败,--helpdoctor 也必须能用。doctor 检查 Python、依赖、NVML、GPU、端口、数据目录与打包的面板, 并为每一项失败给出修复建议,而不是抛一堆栈。status --json 是编辑器消费的接口。

VS Code extension

VS Code 扩展

Defects found while doing this

本轮发现的缺陷

Four problems surfaced that only exist because the tool became installable; they are entries 14–17 in the list below. The most interesting was not a code defect at all: bundling the dashboard inside the package made the SPA fallback answer unknown /api/* paths with HTTP 200 and a page of HTML, so an API typo started looking like success. That is precisely the class of bug that appears when a development-only deployment becomes a product. Two further entries, 18 and 19, come from publishing rather than from the product: a PyPI name that was already taken, and a CI job that linted a directory the refactor had deleted.

有四个问题只有在"变成可安装的工具"之后才会出现,列在下方第 14–17 条。其中最有意思的一个 甚至不算代码缺陷:把面板打进包里之后,SPA 兜底路由开始把未知的 /api/* 返回成 HTTP 200 + 一页 HTML,于是一个 API 拼写错误看起来像调用成功。这正是"开发环境专用部署" 变成"产品"时才会冒出来的那一类问题。另有第 18、19 两条来自"发布"而非"产品"本身: 一个已被占用的 PyPI 名字,以及一个在重构后仍然 lint 已删除目录的 CI 任务。

Where it was validated

验证位置

PathResult
Wheel contents250 KB, includes labwatch/ui; package-data test asserts the dashboard is present and served
uvx equivalent (local wheel and local checkout)starts, serves the bundled UI (HTTP 200 on / and on /assets/*.js)
Editable install, CLI end to enddoctor, start, status, stop, open, version, python -m labwatch
VS Code extensionpackaged as .vsix, installed into VS Code (11 files, 15 KB)
Extension against the real server CLIparser and formatters fed the live 8-GPU payload over SSH; every contract check passed
Lab server, packaged installv1.1.0 installed offline, doctor all green, dashboard served from the package, other users' 8 CUDA processes untouched
Docker buildblocked by this machine's Docker DNS at the time of writing; the Dockerfile was rewritten for the package layout and the CI job verifies it independently
路径结果
wheel 内容250 KB,包含 labwatch/ui;有测试断言面板存在且可被托管
uvx 等价路径(本地 wheel 与本地仓库)可启动,托管打包 UI(//assets/*.js 均 200)
可编辑安装 + CLI 全流程doctor、start、status、stop、open、version、python -m labwatch
VS Code 扩展已打包为 .vsix 并成功安装到 VS Code(11 个文件,15 KB)
扩展对接真实服务器 CLI解析器与格式化函数直接消费经 SSH 取回的真实 8 卡数据,全部契约检查通过
实验服务器上的包化安装v1.1.0 离线安装成功,doctor 全绿,面板由包直接托管,其他用户的 8 个 CUDA 进程未受影响
Docker 构建撰写时被本机 Docker 的 DNS 故障阻塞;Dockerfile 已按包化结构重写,CI 中有独立 job 验证

7. Bugs found and fixed during development

7. 开发过程中发现并修复的缺陷

These were found by the tests, by looking at the rendered UI, or by comparing against the driver — not by reading code alone.

以下缺陷来自测试、来自观察真实渲染的界面、或来自与驱动的对比 —— 而不是仅靠阅读代码发现的。

#BugImpactFix
1 /api/health read last_history_write from a context field that is only assigned during shutdown. A running server always reported null, so the collector looked dead and the value could not be monitored. Read the live value from the history service and expose collector writes, errors, interval and stored point count.
2 Per-process CPU deltas were discarded: the psutil priming cache was cleared on every poll. Process CPU % could never accumulate a real delta and stayed at zero. Keep baselines keyed by pid, prime before enrichment, and prune pids that disappear.
3 NVML graphics-running-processes made the table unusable on Windows (about 30 desktop apps). The GPU process table was dominated by compositing noise instead of workloads. Default to compute processes (matching nvidia-smi --query-compute-apps) with an opt-in flag for graphics contexts.
4 Demo disk usage was computed from hours since the Unix epoch (about 490,000 h). The disk tile showed 99 % critical in every screenshot, which is both wrong and alarming. Anchor the growth curve to the current year plus nine months and clamp it, giving a realistic 62 %.
5 Demo signals were sampled at one second regardless of chart resolution. The 24H chart was an unreadable solid block; the 1H chart looked like a synthetic sine wave. Scale jitter amplitude, jitter period and the load envelope with the requested window.
6 os.getloadavg was called without guarding against TypeError on platforms that expose it as None. CPU collection raised instead of reporting N/A for load average. Treat missing or non-callable implementations as N/A.
7 The screenshot script located sections by loose heading patterns such as /host/i. Playwright strict mode failed once chart titles like "Host utilisation" existed — a latent test-suite break. Target section containers by data-testid, and match section headings exactly.
8 Dockerfile carried a # syntax=docker/dockerfile:1 directive it did not need. The build failed on any host that cannot reach the Docker frontend image registry. Remove the directive; the file only uses portable instructions.
9 Two tests were genuinely flaky: a CPU-burn assumption, and a race where the health check was polled on last_history_write while the first sample was still in flight. Intermittent red builds that would erode trust in CI. Assert deterministic signals, and poll on the definitive completion counter instead.
10 Primary disk detection picked the longest matching mount point, so inside a container it selected /etc/resolv.conf — a 2.5 GB ext4 bind mount. Found only by rendering the dashboard against the real container: the disk tile showed a config file instead of the filesystem. A plausible-looking but meaningless number is worse than no number. Ignore pseudo filesystems and container-injected file mounts, prefer the root filesystem, and otherwise take the shallowest real mount.
14 Bundling the dashboard inside the package made the SPA fallback answer unknown /api/* paths with HTTP 200 and a page of HTML. A typo in an API call returned "success" with HTML body, which is far harder to debug than a 404. Only reachable once a built UI always exists, so the development deployment never showed it. Register an explicit /api/{rest} 404 handler before the SPA catch-all, and test it with a bundle present.
15 The CLI printed check marks to a Windows console using a legacy code page (GBK) and crashed with UnicodeEncodeError. labwatch doctor crashed on the happy path, which is the worst possible moment to fail. Detect whether the output stream can encode the markers, and fall back to ASCII when it cannot.
16 An f-string reused the same quote character inside its expression, which is a syntax error on Python 3.10 — the floor that pyproject.toml claims to support. The CLI would fail to import on the oldest supported interpreter while passing every test on 3.12. Restructured the f-string, and added scripts/check-syntax-floor.py, which parses the package with the 3.10 grammar in CI.
17 Moving the code into a package left 18 relative imports one level too deep, so modules failed to import at runtime. A structural refactor like this breaks imports silently until an affected path runs; the failures surfaced only when the CLI was exercised. scripts/check-imports.py resolves every relative import against the files that exist and runs in CI.
18 The project was prepared to publish to PyPI as labwatch — a name that is already taken there by an unrelated homelab CLI (rbretschneider/labwatch_cli, 49 releases). The availability check had only covered GitHub. The most damaging possible packaging bug: uvx labwatch would either fail outright at upload time or, worse, silently install and run a stranger's tool for anyone following the README. Publish under the distribution name labwatch-lite, keep the console command and import package as labwatch, explain the difference in both READMEs, and add a release-workflow guard that refuses to build or upload unless pyproject.toml declares labwatch-lite and dist/ contains labwatch_lite-* artifacts.
19 The CI backend job ran ruff check labwatch backend after the old backend/ directory was removed during the packaging refactor. A red build with no real lint problem behind it: the failure was "file not found", which sends you reading code instead of the workflow. The package and e2e jobs never ran, so the wheel was not verified either. Correct the invocation to ruff check labwatch tests and add scripts/check-workflow-paths.py, which extracts path-looking arguments from every workflow step and fails when one does not exist.
11 The dashboard only ever showed the root filesystem. Found on the lab server, where /nfs-data1 was 98.4 % full while the UI reported / at 91.7 % and nothing else. The disk that actually matters on a research server is usually a data volume, so the panel hid the one number that needed acting on: a 15 TB volume with 226 GB left was invisible. Report every real filesystem by default (LABWATCH_INCLUDE_ALL_MOUNTS), add a filesystem panel that lists them worst-first, and give the disk tile a mount count instead of duplicating two of them.
12 Memory percentage used psutil's total - available, which counts page cache and shared memory as used. On the lab server it read 23.1 % where free read 8.4 %, because 84 GB of /dev/shm was in use. A dashboard that appears to show a quarter of memory in use when the server is nearly idle teaches the operator to distrust it. Switch to the free accounting (total - free - buffers - cached) and expose free, cached and available so the figure can be reconciled.
13 Process CPU reported 0.0 % on the first sample after startup, because the psutil baseline was milliseconds old. On the lab server that meant showing 0.0 % for eight CUDA processes that ps reported at 107-110 %. Reporting nothing is honest; reporting zero is not. Enforce a minimum baseline age and return no value until a real delta exists. The first sample now shows nothing, subsequent samples show about 100 %.
#缺陷影响修复
1 /api/health 从一个只在关机时才赋值的上下文字段读取 last_history_write 运行中的服务永远返回 null,采集器看起来像挂了,该值也无法用于监控。 改从历史服务读取实时值,并补充暴露写入次数、失败次数、采样间隔与库中点数。
2 逐进程 CPU 差值被丢弃:psutil 的基准缓存每轮轮询都被清空。 进程 CPU % 永远无法累积出真实差值,始终为 0。 按 PID 保留基准、在补全信息之前打基准,并清理消失的 PID。
3 NVML 的图形运行进程接口让进程表在 Windows 上完全不可用(约 30 个桌面程序)。 GPU 进程表被合成器噪声淹没,看不到真正的工作负载。 默认只列计算进程(与 nvidia-smi --query-compute-apps 一致),并保留包含图形上下文的开关。
4 演示数据的磁盘用量按"Unix 纪元至今的小时数"计算(约 49 万小时)。 每张截图里磁盘卡片都显示 99 % 严重,既错误又刺眼。 把增长曲线锚定到"当前年份 + 9 个月"并加夹取,得到真实的 62 %。
5 演示信号无论图表分辨率如何都按 1 秒采样。 24H 图糊成一块无法阅读的实心块;1H 图则像标准正弦波,一眼假。 让抖动幅度、抖动周期与负载包络都随所请求的时间窗口缩放。
6 调用 os.getloadavg 时未防御 TypeError,而某些平台把它暴露为 None CPU 采集直接抛异常,而不是把负载均值报为 N/A 把缺失或不可调用的实现一律视为 N/A
7 截图脚本用 /host/i 这类宽松的标题匹配定位区块。 一旦出现 "Host utilisation" 这类图表标题,Playwright 严格模式就会失败 —— 一颗埋着的测试隐患。 改为通过 data-testid 定位区块容器,并让区块标题精确匹配。
8 Dockerfile 带了一个并不需要的 # syntax=docker/dockerfile:1 指令。 在无法访问 Docker 前端镜像仓库的主机上,构建直接失败。 删除该指令;文件本身只使用可移植指令。
9 两个测试确实不稳定:一个依赖"烧 CPU"的假设,另一个在首个采样尚未完成时就轮询 last_history_write 间歇性红灯会侵蚀对 CI 的信任。 改为断言确定性信号,并以明确的完成计数器作为轮询条件。
10 主磁盘探测选择"匹配路径最长"的挂载点,于是在容器内选中了 /etc/resolv.conf —— 一个 2.5 GB 的 ext4 单文件挂载。 只有真正对着容器渲染面板才发现:磁盘卡片显示的是一个配置文件而不是文件系统。一个看起来合理却毫无意义的数字,比没有数字更糟。 排除伪文件系统与容器注入的文件挂载,优先根文件系统,否则取路径最浅的真实挂载点。
14 把面板打进包里之后,SPA 兜底路由开始把未知的 /api/* 返回成 HTTP 200 加一页 HTML。 一个 API 拼写错误会返回"成功"但内容是 HTML,比 404 难排查得多。只有在"构建产物总是存在"之后才会触发,所以开发环境从未暴露。 在 SPA 兜底之前显式注册 /api/{rest} 的 404 处理,并在存在构建产物的场景下加测试。
15 CLI 向使用旧代码页(GBK)的 Windows 控制台输出勾号时,抛出 UnicodeEncodeError 崩溃。 labwatch doctor 在最顺利的路径上崩溃 —— 这是最不该出错的时刻。 先探测输出流能否编码这些符号,不能则退化为 ASCII。
16 某个 f-string 在表达式内复用了相同的引号字符,这在 Python 3.10 上是语法错误 —— 而 pyproject.toml 声称支持 3.10。 CLI 在最低支持版本上无法导入,但在 3.12 上所有测试都通过。 改写该 f-string,并新增 scripts/check-syntax-floor.py,在 CI 中用 3.10 语法解析整个包。
17 把代码搬进包结构后,18 处相对导入多了一层,导致模块在运行时无法导入。 这类结构重构会静默破坏导入,直到受影响的路径被执行;这里是靠实际跑 CLI 才暴露的。 新增 scripts/check-imports.py,把每个相对导入与真实存在的文件做解析校验,并纳入 CI。
18 项目本来准备以 labwatch 为名发布到 PyPI —— 但该名字已被一个无关的自建服务器 CLI 占用(rbretschneider/labwatch_cli,49 个版本)。此前的可用性检查只覆盖了 GitHub。 这是打包环节最严重的一类缺陷:uvx labwatch 要么在上传时直接失败,要么更糟 —— 让照 README 操作的人静默装上并运行别人的工具。 改用发行名 labwatch-lite 发布,命令行命令与导入包名仍为 labwatch;在两份 README 中说明差异;并在发布工作流中加入守卫:若 pyproject.toml 声明的不是 labwatch-lite,或 dist/ 中没有 labwatch_lite-* 产物,则拒绝构建与上传。
19 打包重构删除了旧的 backend/ 目录后,CI 的后端任务仍在执行 ruff check labwatch backend 红灯背后并没有真实的 lint 问题:报错是"找不到文件",会让人去读代码而不是读工作流。同时 packagee2e 任务没有运行,等于 wheel 也没被验证。 把命令改为 ruff check labwatch tests,并新增 scripts/check-workflow-paths.py:从每个工作流步骤中抽取形似路径的参数,一旦不存在即失败。
11 面板此前只显示根文件系统。在实验服务器上发现:/nfs-data1 已用 98.4 %,而界面只报 / 的 91.7 %,其余一概不显示。 科研服务器上真正要紧的磁盘通常是数据盘,因此面板恰好隐藏了唯一需要处理的那个数字:一个还剩 226 GB 的 15 TB 卷完全不可见。 默认上报全部真实文件系统(LABWATCH_INCLUDE_ALL_MOUNTS);新增按使用率降序排列的文件系统面板;磁盘卡片改为显示挂载点数量,不再重复两个。
12 内存百分比使用了 psutiltotal - available,该口径把页缓存与共享内存算作已用。在实验服务器上它显示 23.1 %,而 free 显示 8.4 %,原因是 84 GB 的 /dev/shm 正在使用。 服务器几乎空闲时面板却像有四分之一内存压力,会让使用者逐渐不再信任这个面板。 改用 free 的口径(total - free - buffers - cached),并额外暴露 freecachedavailable 便于核对。
13 启动后第一次采样中进程 CPU 报 0.0 %,因为 psutil 的基准只存在了几毫秒。 在实验服务器上,这意味着把 ps 显示 107–110 % 的八个 CUDA 进程报成 0.0 %。不报是诚实的,报 0 不是。 强制基准的最小存在时长,在形成真实差值前不返回数值。现在首次采样显示空,后续采样约 100 %。

8. Known limitations

8. 已知限制

LimitationReason / mitigation
Single host onlyAn explicit v1 scope decision. Multi-server aggregation is on the roadmap.
No authenticationIntended for trusted private networks. The README warns that process command lines may be sensitive. Put it behind an authenticating reverse proxy if exposed.
NVIDIA onlyTelemetry comes from NVML. AMD and Intel GPUs are not read.
Noisy graphics contexts on WindowsWDDM exposes every compositing application as a compute process. Linux targets are unaffected; an opt-in flag includes graphics contexts.
Sampled, not streamed historyA 10 second write interval can miss brief spikes. Lower LABWATCH_HISTORY_INTERVAL if that matters.
Load average is N/A on WindowsThe platform does not expose it; the UI shows N/A rather than a fabricated zero.
Per-process CPU needs two pollsCPU usage is a counter delta, so the first sample after startup reports N/A for process CPU. Documented behaviour, not a defect.
Container disk figures describe the containerWhen the root filesystem is an overlay (Docker Desktop), LabWatch falls back to the shallowest real mount, which may be a data volume. Run LabWatch natively for host disk numbers.
Multiple GPUs verified in demo mode onlyThis development machine has one GPU, so multi-device rendering was exercised with three synthetic devices. The collector enumerates NVML devices generically, with no single-device assumption.
限制原因 / 应对
仅支持单机这是 v1 明确的范围决策。多机聚合在路线图中。
没有认证面向可信内网。README 已提醒进程命令行可能包含敏感信息。若要对外暴露,请置于带认证的反向代理之后。
仅支持 NVIDIA遥测来自 NVML,不读取 AMD 与 Intel GPU。
Windows 上图形上下文噪声大WDDM 会把每个合成程序都暴露为计算进程。目标平台 Linux 不受影响;提供开关以包含图形上下文。
历史是采样的,不是流式的10 秒写入间隔可能漏掉瞬时尖峰。如有需要可调低 LABWATCH_HISTORY_INTERVAL
Windows 上负载均值为 N/A平台不提供该数据;界面显示 N/A,而不是编造一个 0。
逐进程 CPU 需要两次采样CPU 使用率是计数器差值,因此启动后的第一次采样中进程 CPU 报 N/A。这是有意的行为,不是缺陷。
容器内的磁盘数字描述容器本身当根文件系统是 overlay(Docker Desktop)时,LabWatch 退回最浅的真实挂载点,可能是数据卷。需要宿主机磁盘数字请直接在宿主机运行。
多卡仅在演示模式下验证过本开发机只有一张 GPU,因此多设备渲染是通过 3 个合成设备验证的。采集器采用通用 NVML 设备枚举,没有任何单卡假设。

9. Repository hygiene

9. 仓库整洁度

10. Release readiness

10. 发布就绪度

Definition-of-done itemStatus
Core monitoring worksverified against the driver
Multiple GPUs on one hostper-device cards, series and filters
GPU process mapping worksNVML pids joined to psutil detail
Historical charts work1H/6H/24H, persisted in SQLite
No-GPU failure is gracefultested; host metrics keep working
Tests pass179 + 77 + 8 + 13
CI passeslint, tests, build, E2E, Docker smoke test
Docker worksbuilt, smoke tested, and run with GPU passthrough
README completefeatures, quick start, architecture, config, API, limitations, roadmap (English + 中文)
Screenshots completetwelve, all referenced, including real hardware
Architecture diagram completedocs/architecture.svg
Demo mode worksand is clearly labelled in the UI
No major console or API errorsasserted by the E2E suite and the capture script
Repository cleansee section 7
Publishable name available on PyPIpublished as distribution labwatch-lite; the console command stays labwatch
Sources pushedGitHub main, CI 6/6 green on every commit
完成度检查项状态
核心监控可用已与驱动实测比对
单机多卡逐卡卡片、序列与过滤
GPU 进程映射可用NVML PID 与 psutil 信息关联
历史图表可用1H/6H/24H,持久化于 SQLite
无 GPU 时优雅降级已测试;主机指标照常工作
测试通过179 + 77 + 8 + 13
CI 通过lint、测试、构建、E2E、Docker 冒烟测试
Docker 可用已构建、已冒烟测试、已验证 GPU 直通
README 完整功能、快速开始、架构、配置、API、限制、路线图(英文 + 中文)
截图完整12 张,全部被引用,含真实硬件
架构图完整docs/architecture.svg
演示模式可用并在界面中明确标注
无重大 console / API 报错由 E2E 套件与截图脚本断言
仓库整洁见第 7 节
PyPI 上可用的发布名以发行名 labwatch-lite 发布;命令行命令仍为 labwatch
源码已推送GitHub main,每次提交 CI 均 6/6 全绿
Verdict: LabWatch v1.1.0 meets its definition of done and is ready to publish. It was validated on a real 8 x RTX 4090 research server under live experimental load, in addition to the single-GPU development host and the Docker image with GPU passthrough. That validation found and fixed three genuine defects, all listed in section 6.
结论:LabWatch v1.1.0 已满足完成度定义,可以公开发布。除单卡开发机与 Docker GPU 直通之外,还在真实的 8 × RTX 4090 科研服务器上、于实验负载运行期间完成了验证。 正是这次验证发现并修复了三个真实缺陷,详见第 6 节。