fix: 速度计算按真实时间间隔而非假定为1秒
calculateSpeed() 原直接取字节差作为 bytes/sec,假定定时器严格 1 秒触发。系统负载高时 QTimer 延迟会导致速度失真。 改为记录上次采样的毫秒时间戳 m_lastTimestampMs,按真实流逝时间 delta_bytes / elapsed_seconds 计算速度;速度存储由 qint64 改为 double 保留精度;elapsedSec > 0 守卫避免除零。 同时新增 docs/project-review.md 项目审查清单。
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# JNetApplet 项目审查清单
|
||||
|
||||
> 审查日期:2026-07-22
|
||||
> 审查范围:全部源码(C++ 后端 + QML 前端 + 构建脚本 + 文档)
|
||||
|
||||
---
|
||||
|
||||
## 一、缺陷(Bug / 逻辑错误)
|
||||
|
||||
### 🔴 高优先级
|
||||
|
||||
**1. ~~速度计算假设定时间隔严格为 1 秒~~ ✅ 已修复**
|
||||
~~`calculateSpeed()` 直接用 `currentRxBytes - m_lastRxBytes` 作为 bytes/sec,未除以实际流逝时间。系统负载高或定时器抖动时,速度值会失真。~~
|
||||
已改为记录上次采样的毫秒时间戳 `m_lastTimestampMs`,按真实流逝时间 `delta_bytes / elapsed_seconds` 计算速度;速度存储由 `qint64` 改为 `double` 保留精度;`elapsedSec > 0` 守卫避免除零。
|
||||
|
||||
**2. 计数器回绕/重置产生负速度**
|
||||
网卡重启、`/proc/net/dev` 计数器溢出或接口重置时,`currentRxBytes - m_lastRxBytes` 可能为负。代码无任何兜底,QML 会显示负速度。应加 `if (delta < 0) delta = 0;` 保护。
|
||||
|
||||
**3. "总量统计"语义有误导**
|
||||
`m_totalDownload = currentRxBytes` 只存储活动接口的当前计数器值,并非真正的累计流量。它会在以下场景跳变:切换网卡(跳到新接口计数器)、网卡重启(归零)、系统重启(归零)。README 宣称"累计上传/下载流量统计",但实际不是跨重启/跨接口的累计。
|
||||
|
||||
**4. 独立窗口不支持深色模式**
|
||||
`AboutWindow.qml`、`SettingsWindow.qml`、`TextColorPicker.qml`、`TrafficChartWindow.qml` 全部硬编码浅色(`#FFFFFF`、`#333333`、`#f5f5f5` 等),不随系统主题适配。而 `networkview.qml` 和 `NetworkPopup.qml` 正确地从 `DockPalette` 派生颜色。deepin 深色模式下这些窗口视觉割裂严重。
|
||||
|
||||
**5. `detectInterfaces()` 过滤规则与 `readNetworkStats()` 不一致**
|
||||
`init()` 先调 `detectInterfaces()`(读 `/sys/class/net`,仅过滤 `lo`),再调 `readNetworkStats()`(读 `/proc/net/dev`,过滤 `lo`/`veth`/`docker`/`br-`)。首次刷新前,docker/veth 接口会短暂出现在列表中,造成 UI 闪烁。且 `detectInterfaces()` 的结果会被 `readNetworkStats()` 完全覆盖,属于冗余调用。
|
||||
|
||||
### 🟡 中优先级
|
||||
|
||||
**6. `setActiveInterface()` 不校验接口有效性**
|
||||
可传入不存在的接口名,会被持久化到配置文件。下次启动时虽然会被 `readNetworkStats()` 清空回退,但当前会话期间 `getActiveRxBytes()` 返回 0,速度恒为 0,用户困惑。
|
||||
|
||||
**7. 速度历史仅记录活动接口**
|
||||
`m_speedHistory` 每秒只给 `m_activeInterface` 追加采样点。非活动接口永不采集,切换到新接口时趋势图为空,需等待数分钟才有数据。若要"切回仍能看到历史",应后台为所有接口持续采样。
|
||||
|
||||
**8. 窗口居中未考虑多显示器**
|
||||
`AboutWindow`、`SettingsWindow`、`TrafficChartWindow` 用 `Screen.width/height` 居中,frameless 窗口在多屏环境下可能出现在非预期屏幕。
|
||||
|
||||
**9. SPDX 版权归属为占位符**
|
||||
所有文件头 `SPDX-FileCopyrightText: 2024 MyCompany`,"MyCompany" 是模板占位符,应改为实际版权持有者(如 `2024 Jokul`)。
|
||||
|
||||
**10. `version()` 无兜底**
|
||||
若 `pluginMetaData().value("Version")` 返回空(元数据未加载),AboutWindow 版本行显示空白。networkview.qml 有 `"1.0"` 兜底,但 AboutWindow 的 `version` 属性直接来自 `applet.version`,无兜底。
|
||||
|
||||
---
|
||||
|
||||
## 二、代码质量 / 可维护性
|
||||
|
||||
**11. 大量重复代码跨组件复制**
|
||||
以下代码在 `networkview.qml` 和 `NetworkPopup.qml` 中完全重复,`formatSpeed` 还在 `TrafficChartWindow.qml` 中第三份拷贝:
|
||||
- `isPhysicalIf()` 函数
|
||||
- `sortedInterfaces` 属性
|
||||
- `formatSpeed()` / `formatTotal()` 函数
|
||||
- 全套调色板定义(`basePalette`、`primaryText`、`accentBlue`…)
|
||||
- `downloadValueColor` / `uploadValueColor`
|
||||
|
||||
改一处需同步改 2-3 处,极易遗漏。应抽取为公共 QML 文件(如 `Theme.qml` 单例 + `Format.js`)。
|
||||
|
||||
**12. 死代码:`interfaceStats` 属性未被使用**
|
||||
`networkview.qml:43` 绑定了 `interfaceStats` 属性,但从未在任何 UI 中读取。C++ 侧的 `interfaceStats()` 函数和 `Q_PROPERTY` 也是死代码。且其 `QStringList` 用 `|` 分隔符编码结构化数据的方式本身也很脆弱。
|
||||
|
||||
**13. 死代码:`refresh()` Q_INVOKABLE 从未被 QML 调用**
|
||||
注释(networkview.qml:276)说明此前 QML 调 `applet.refresh()` 导致问题后已移除调用,但 C++ 侧的 `Q_INVOKABLE void refresh()` 仍保留。若确无外部调用方,可删除或标注保留原因。
|
||||
|
||||
**14. `interfaceStats` 暴露的数据不完整**
|
||||
`NetworkInterface` 结构体解析了 `rxErrors`/`txErrors`/`rxDropped`/`txDropped`,但 `interfaceStats()` 输出时丢弃了这些字段。若未来要展示丢包/错误率,需补全。
|
||||
|
||||
**15. README.md 和 AGENTS.md 严重过时**
|
||||
- 项目结构只列 `AboutWindow.qml`,实际有 5 个组件(缺 `NetworkPopup`、`SettingsWindow`、`TextColorPicker`、`TrafficChartWindow`)
|
||||
- README 功能特性未提及:设置窗口、字体颜色自定义、流量波动图、IPv6 显示
|
||||
- README 写"支持从弹窗一键卸载插件",但卸载功能实际在设置窗口
|
||||
- AGENTS.md "Project structure" 同样过时
|
||||
|
||||
**16. `qsTr()` 源字符串为中文,无翻译基础设施**
|
||||
QML 中 `qsTr("网络速度监控")` 等以中文为源串,但项目无 `.ts` 翻译文件、无 `lupdate`/`lrelease` 构建步骤、C++ 无翻译加载逻辑。`qsTr()` 实质为空操作。若仅面向中文用户可接受;若计划国际化,需补全 i18n 基础设施并以英文为源串。
|
||||
|
||||
**17. C++ 后端无单元测试**
|
||||
项目无任何测试。`calculateSpeed`、`readNetworkStats` 的正则解析、`isPhysicalInterface`、`niceCeil`(QML)等纯逻辑函数适合且应该有单元测试覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 三、可开发的新需求 / 功能增强
|
||||
|
||||
### 🟢 推荐开发
|
||||
|
||||
| # | 需求 | 说明 | 价值 |
|
||||
|---|------|------|------|
|
||||
| 18 | **日/月流量统计持久化** | 将每日累计流量写入 `settings.ini`,重启后保留。弹窗/设置窗口展示今日/本月用量 | 解决缺陷 #3,核心功能提升 |
|
||||
| 19 | **深色模式适配** | AboutWindow/SettingsWindow/TextColorPicker/TrafficChartWindow 改用 DTK 主题色或 `DockPalette` 派生色 | 解决缺陷 #4,视觉一致性 |
|
||||
| 20 | **公共代码抽取** | 颜色/格式化/排序逻辑抽取为共享文件,消除 3 处重复 | 解决 #11,降低维护成本 |
|
||||
| 21 | **文档同步** | 更新 README + AGENTS.md 的项目结构、功能列表、组件说明 | 解决 #15,新人/AI 接手必备 |
|
||||
| 22 | **网络断连检测** | 活动接口 `operState != Up` 或 IP 丢失时,任务栏显示断连图标 + tooltip 提示 | 用户体验提升 |
|
||||
| 23 | **全接口持续采样** | 后台为所有接口采集速度历史,切换接口时趋势图立即有数据 | 解决缺陷 #7 |
|
||||
| 24 | **速度单位切换** | 设置中可选 KB/s(二进制)或 Mbps(十进制),或智能自动切换 | 国际化/习惯适配 |
|
||||
|
||||
### 🔵 可选增强
|
||||
|
||||
| # | 需求 | 说明 |
|
||||
|---|------|------|
|
||||
| 25 | **刷新间隔可配置** | 设置中可选 1s/2s/5s 刷新频率,降低低端设备 CPU 占用 |
|
||||
| 26 | **流量超限通知** | 设置阈值,下载/上传超限时发送系统通知 |
|
||||
| 27 | **趋势图时间窗口可选** | 流量波动图支持 5min / 30min / 1h 切换(需持久化历史数据) |
|
||||
| 28 | **多接口聚合速度** | 任务栏可选显示所有接口合计速度,而非仅活动接口 |
|
||||
| 29 | **HiDPI 适配** | 独立窗口使用相对尺寸而非固定像素,适配高分辨率屏幕 |
|
||||
| 30 | **i18n 翻译基础设施** | 源串改英文 + 添加中文 `.ts` + `lrelease` 构建集成 |
|
||||
| 31 | **DTK 原生窗口框架** | 独立窗口改用 `DWindow` / DTK 窗口装饰,获得原生 deepin 标题栏、圆角、阴影 |
|
||||
| 32 | **导出流量数据** | 将流量统计导出为 CSV/JSON,便于分析 |
|
||||
| 33 | **C++ 后端单元测试** | 为速度计算、正则解析、接口检测添加 Qt Test 单元测试 |
|
||||
| 34 | **丢包/错误率展示** | 弹窗中展示 `rxErrors`/`rxDropped`,网络质量诊断 |
|
||||
| 35 | **开机自启保障** | 确保 dde-shell 启动时插件自动加载(可能需 dde-shell 配置) |
|
||||
|
||||
---
|
||||
|
||||
## 优先级建议
|
||||
|
||||
**第一优先级**(影响正确性):#1 速度计算间隔、#2 负速度、#5 接口过滤不一致
|
||||
|
||||
**第二优先级**(影响体验):#4 深色模式、#3 总量语义、#7 历史采样、#15 文档过时
|
||||
|
||||
**第三优先级**(工程质量):#11 重复代码、#12-14 死代码、#17 测试缺失
|
||||
|
||||
**第四优先级**(功能扩展):#18 日/月统计、#22 断连检测、#26 流量通知
|
||||
@@ -22,6 +22,7 @@ NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent)
|
||||
, m_refreshTimer(nullptr)
|
||||
, m_lastRxBytes(0)
|
||||
, m_lastTxBytes(0)
|
||||
, m_lastTimestampMs(0)
|
||||
, m_downloadSpeed(0)
|
||||
, m_uploadSpeed(0)
|
||||
, m_totalDownload(0)
|
||||
@@ -79,12 +80,12 @@ bool NetworkMonitorApplet::init()
|
||||
|
||||
double NetworkMonitorApplet::downloadSpeed() const
|
||||
{
|
||||
return static_cast<double>(m_downloadSpeed);
|
||||
return m_downloadSpeed;
|
||||
}
|
||||
|
||||
double NetworkMonitorApplet::uploadSpeed() const
|
||||
{
|
||||
return static_cast<double>(m_uploadSpeed);
|
||||
return m_uploadSpeed;
|
||||
}
|
||||
|
||||
double NetworkMonitorApplet::totalDownload() const
|
||||
@@ -354,32 +355,44 @@ void NetworkMonitorApplet::calculateSpeed()
|
||||
{
|
||||
qint64 currentRxBytes = getActiveRxBytes();
|
||||
qint64 currentTxBytes = getActiveTxBytes();
|
||||
// 当前采样时间戳(毫秒),用于按真实流逝时间计算速度
|
||||
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
if (m_firstUpdate) {
|
||||
m_firstUpdate = false;
|
||||
m_lastRxBytes = currentRxBytes;
|
||||
m_lastTxBytes = currentTxBytes;
|
||||
m_lastTimestampMs = nowMs;
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算速度(字节/秒)
|
||||
m_downloadSpeed = currentRxBytes - m_lastRxBytes;
|
||||
m_uploadSpeed = currentTxBytes - m_lastTxBytes;
|
||||
// 按真实流逝时间计算速度(字节/秒)
|
||||
// 设计原因:原实现假设定时间隔严格 1 秒,直接取字节差作为速度;
|
||||
// 系统负载高时 QTimer 可能延迟触发,实际间隔偏离 1 秒导致速度失真。
|
||||
// 改为除以真实间隔后,无论定时器抖动如何速度都准确。
|
||||
const double elapsedSec = (nowMs - m_lastTimestampMs) / 1000.0;
|
||||
|
||||
// 更新总量
|
||||
m_totalDownload = currentRxBytes;
|
||||
m_totalUpload = currentTxBytes;
|
||||
|
||||
// 仅当间隔合法时计算速度,避免除零;间隔为 0 时保持上次速度值
|
||||
if (elapsedSec > 0) {
|
||||
m_downloadSpeed = (currentRxBytes - m_lastRxBytes) / elapsedSec;
|
||||
m_uploadSpeed = (currentTxBytes - m_lastTxBytes) / elapsedSec;
|
||||
}
|
||||
|
||||
m_lastRxBytes = currentRxBytes;
|
||||
m_lastTxBytes = currentTxBytes;
|
||||
m_lastTimestampMs = nowMs;
|
||||
|
||||
// 追加采样点到当前活动接口的环形缓冲
|
||||
// 设计原因:applet 启动即持续采集,用户随时打开趋势图都能看到完整 5 分钟数据
|
||||
if (!m_activeInterface.isEmpty()) {
|
||||
SpeedSample sample;
|
||||
sample.timestamp = QDateTime::currentSecsSinceEpoch();
|
||||
sample.downloadSpeed = static_cast<double>(m_downloadSpeed);
|
||||
sample.uploadSpeed = static_cast<double>(m_uploadSpeed);
|
||||
sample.downloadSpeed = m_downloadSpeed;
|
||||
sample.uploadSpeed = m_uploadSpeed;
|
||||
m_speedHistory[m_activeInterface].append(sample);
|
||||
|
||||
// 滑动窗口裁剪:超过 300 点时丢弃最旧的
|
||||
|
||||
@@ -135,8 +135,11 @@ private:
|
||||
// 速度计算
|
||||
qint64 m_lastRxBytes;
|
||||
qint64 m_lastTxBytes;
|
||||
qint64 m_downloadSpeed;
|
||||
qint64 m_uploadSpeed;
|
||||
// 上次采样的毫秒时间戳,用于按真实间隔计算速度(避免定时器抖动失真)
|
||||
qint64 m_lastTimestampMs;
|
||||
// 当前瞬时速度(字节/秒),用 double 存储以保留按真实间隔除算的小数精度
|
||||
double m_downloadSpeed;
|
||||
double m_uploadSpeed;
|
||||
|
||||
// 总量
|
||||
qint64 m_totalDownload;
|
||||
|
||||
Reference in New Issue
Block a user