fix: 为所有接口采集速度历史而非仅活动接口

原 calculateSpeed 仅给活动接口追加采样点,切换到非活动接口时趋势图
为空需等待数分钟。新增 m_lastRxBytesByIface/m_lastTxBytesByIface
跟踪所有接口字节基线,遍历全部接口采集速度历史。

切换接口时趋势图立即有数据,新增接口(如USB网卡)也自动开始采集。
This commit is contained in:
2026-07-24 09:53:39 +08:00
parent 8e7b945a7c
commit 9803c59e76
3 changed files with 56 additions and 41 deletions
+3 -2
View File
@@ -35,8 +35,9 @@
~~可传入不存在的接口名,会被持久化到配置文件,当前会话期间速度恒为 0。~~
已在方法入口增加 `m_interfaceList.contains(interface)` 校验,无效接口名直接忽略。
**7. 速度历史仅记录活动接口**
`m_speedHistory` 每秒只给 `m_activeInterface` 追加采样点非活动接口永不采集,切换到新接口时趋势图为空,需等待数分钟才有数据。若要"切回仍能看到历史",应后台为所有接口持续采样
**7. ~~速度历史仅记录活动接口~~ ✅ 已修复**
~~`m_speedHistory` 每秒只给 `m_activeInterface` 追加采样点非活动接口永不采集,切换到新接口时趋势图为空。~~
已新增 `m_lastRxBytesByIface` / `m_lastTxBytesByIface` 跟踪所有接口的字节基线,`calculateSpeed()` 遍历所有接口采集速度历史。切换接口时趋势图立即有数据。
**8. 窗口居中未考虑多显示器**
`AboutWindow``SettingsWindow``TrafficChartWindow``Screen.width/height` 居中,frameless 窗口在多屏环境下可能出现在非预期屏幕。
+44 -34
View File
@@ -303,12 +303,14 @@ void NetworkMonitorApplet::readNetworkStats()
m_interfaceList = newInterfaces.keys();
m_interfaces = newInterfaces;
// 清理已消失接口的历史缓冲,避免内存泄漏
// 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据不再需要
// 清理已消失接口的历史缓冲和基线数据,避免内存泄漏
// 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据和基线不再需要
const QStringList oldIfaces = m_speedHistory.keys();
for (const QString &name : oldIfaces) {
if (!newInterfaces.contains(name)) {
m_speedHistory.remove(name);
m_lastRxBytesByIface.remove(name);
m_lastTxBytesByIface.remove(name);
}
}
@@ -361,61 +363,69 @@ void NetworkMonitorApplet::readNetworkStats()
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;
}
// 按真实流逝时间计算速度(字节/秒)
// 设计原因:原实现假设定时间隔严格 1 秒,直接取字节差作为速度;
// 系统负载高时 QTimer 可能延迟触发,实际间隔偏离 1 秒导致速度失真。
// 改为除以真实间隔后,无论定时器抖动如何速度都准确。
const qint64 nowSec = QDateTime::currentSecsSinceEpoch();
const double elapsedSec = (nowMs - m_lastTimestampMs) / 1000.0;
m_lastTimestampMs = nowMs;
// 仅当间隔合法时计算速度并累加会话流量,避免除零;间隔为 0 时保持上次值
if (elapsedSec > 0) {
// ---- 活动接口速度计算(用于任务栏显示和总量累加)----
if (m_firstUpdate) {
// 首次更新或接口切换后:仅记录基线,不计算速度
m_firstUpdate = false;
m_lastRxBytes = getActiveRxBytes();
m_lastTxBytes = getActiveTxBytes();
} else if (elapsedSec > 0 && !m_activeInterface.isEmpty()) {
// 按真实流逝时间计算速度(字节/秒),避免定时器抖动失真
const qint64 currentRxBytes = getActiveRxBytes();
const qint64 currentTxBytes = getActiveTxBytes();
// 计数器回绕/接口重置时差值可能为负,钳制为 0 避免显示负速度
// 触发场景:网卡重启、/proc/net/dev 计数器溢出、USB 网卡拔出重插
const qint64 rxDelta = currentRxBytes - m_lastRxBytes;
const qint64 txDelta = currentTxBytes - m_lastTxBytes;
const qint64 rxDeltaClamped = rxDelta > 0 ? rxDelta : 0;
const qint64 txDeltaClamped = txDelta > 0 ? txDelta : 0;
m_downloadSpeed = rxDeltaClamped / elapsedSec;
m_uploadSpeed = txDeltaClamped / elapsedSec;
// 累加会话总量:原实现直接取计数器值,切换网卡/网卡重启时总量跳变;
// 改为累加增量后,总量 = 本次会话期间所有活动接口的流量总和,不再跳变
// 累加会话总量:总量 = 本次会话期间所有活动接口的流量总和
m_totalDownload += rxDeltaClamped;
m_totalUpload += txDeltaClamped;
}
m_lastRxBytes = currentRxBytes;
m_lastTxBytes = currentTxBytes;
m_lastTimestampMs = nowMs;
}
// ---- 为所有接口采集速度历史(非仅活动接口)----
// 设计原因:原仅采集活动接口,切换到非活动接口时趋势图为空需等待数分钟。
// 改为遍历所有接口计算各自速度并追加历史,切换接口时趋势图立即有数据。
if (elapsedSec > 0) {
for (const QString &name : m_interfaceList) {
if (!m_interfaces.contains(name)) continue;
const NetworkInterface &iface = m_interfaces[name];
// 接口尚未初始化基线(首次采集或运行中新增的接口),记录基线跳过本次
if (!m_lastRxBytesByIface.contains(name)) {
m_lastRxBytesByIface[name] = iface.rxBytes;
m_lastTxBytesByIface[name] = iface.txBytes;
continue;
}
const qint64 iRxDelta = iface.rxBytes - m_lastRxBytesByIface[name];
const qint64 iTxDelta = iface.txBytes - m_lastTxBytesByIface[name];
// 追加采样点到当前活动接口的环形缓冲
// 设计原因:applet 启动即持续采集,用户随时打开趋势图都能看到完整 5 分钟数据
if (!m_activeInterface.isEmpty()) {
SpeedSample sample;
sample.timestamp = QDateTime::currentSecsSinceEpoch();
sample.downloadSpeed = m_downloadSpeed;
sample.uploadSpeed = m_uploadSpeed;
m_speedHistory[m_activeInterface].append(sample);
sample.timestamp = nowSec;
sample.downloadSpeed = (iRxDelta > 0 ? iRxDelta : 0) / elapsedSec;
sample.uploadSpeed = (iTxDelta > 0 ? iTxDelta : 0) / elapsedSec;
m_speedHistory[name].append(sample);
// 滑动窗口裁剪:超过 300 点时丢弃最旧的
// removeFirst 是 O(n),但 300 点每秒一次开销可忽略,无需复杂环形索引
QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
QVector<SpeedSample> &samples = m_speedHistory[name];
while (samples.size() > MAX_HISTORY_SAMPLES) {
samples.removeFirst();
}
m_lastRxBytesByIface[name] = iface.rxBytes;
m_lastTxBytesByIface[name] = iface.txBytes;
}
emit speedHistoryChanged();
}
+4
View File
@@ -140,6 +140,10 @@ private:
// 当前瞬时速度(字节/秒),用 double 存储以保留按真实间隔除算的小数精度
double m_downloadSpeed;
double m_uploadSpeed;
// 每个接口的上次收发字节数,用于为所有接口(非仅活动接口)计算速度并采集历史
// 设计原因:原仅跟踪活动接口,切换到非活动接口时趋势图为空需等待数分钟才有数据
QHash<QString, qint64> m_lastRxBytesByIface;
QHash<QString, qint64> m_lastTxBytesByIface;
// 总量
qint64 m_totalDownload;