336 lines
20 KiB
C++
336 lines
20 KiB
C++
#include "networktrafficwindow.h"
|
|
#include "../transport/gui_bridge.h"
|
|
#include "../../../src/chat/chat_network_stats.h"
|
|
#include "../../../lib/debug_config.h"
|
|
|
|
#include <QCloseEvent>
|
|
#include <QCoreApplication>
|
|
#include <QGridLayout>
|
|
#include <QHostAddress>
|
|
#include <QLabel>
|
|
#include <QPainter>
|
|
#include <QPainterPath>
|
|
#include <QScrollArea>
|
|
#include <QSet>
|
|
#include <QTimer>
|
|
#include <QToolButton>
|
|
#include <QVBoxLayout>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <vector>
|
|
|
|
static QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
|
|
static constexpr uint64_t HistoryMs = 30000, RateMs = 1000, MaxGapMs = 750;
|
|
static NetworkTrafficWindow* activeMonitor = nullptr;
|
|
|
|
struct TrafficSample {
|
|
uint64_t at, tx, rx, inflight;
|
|
double txRate, rxRate;
|
|
bool gap;
|
|
};
|
|
struct TrafficSeries {
|
|
QColor color;
|
|
unsigned number = 0; // Номер в легенде; поколение используется только как ключ истории.
|
|
QToolButton* button = nullptr; // Владеет панель; checked управляет всеми тремя кривыми.
|
|
std::deque<TrafficSample> samples; // Ограниченная история и накопительные счётчики для секундного среднего.
|
|
};
|
|
|
|
/* Единственный холст панели: одна временная шкала, независимый масштаб каждого ряда. */
|
|
class TrafficCanvas : public QWidget {
|
|
public:
|
|
explicit TrafficCanvas(QWidget* parent) : QWidget(parent) { setObjectName("trafficCanvas"); setFixedHeight(366); }
|
|
QMap<quint64, TrafficSeries> series;
|
|
uint64_t now = 0;
|
|
protected:
|
|
void paintEvent(QPaintEvent*) override;
|
|
};
|
|
|
|
/* Владеет историей одного подключения и кнопками общей легенды; DOWN не удаляет историю. */
|
|
class TrafficConnection : public QWidget {
|
|
public:
|
|
explicit TrafficConnection(quint64 id, QWidget* parent) : QWidget(parent), m_id(id) {
|
|
setObjectName(QStringLiteral("trafficConnection_%1").arg(id, 0, 16));
|
|
auto* layout = new QVBoxLayout(this); layout->setContentsMargins(8, 8, 8, 8); layout->setSpacing(4);
|
|
m_title = new QLabel(this); m_title->setTextFormat(Qt::PlainText);
|
|
QFont font = m_title->font(); font.setBold(true); m_title->setFont(font); layout->addWidget(m_title);
|
|
m_legend = new QGridLayout; m_legend->setContentsMargins(0, 0, 0, 0); m_legend->setSpacing(4);
|
|
m_legend->setAlignment(Qt::AlignLeft); layout->addLayout(m_legend);
|
|
m_canvas = new TrafficCanvas(this); layout->addWidget(m_canvas);
|
|
}
|
|
|
|
void apply(const chat_network_stats_conn& conn, const std::vector<chat_network_stats_link>& links, uint64_t at) {
|
|
if (at <= m_canvas->now) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "traffic graph: stale snapshot connection=%llx at=%llu previous=%llu",
|
|
(unsigned long long)m_id, (unsigned long long)at, (unsigned long long)m_canvas->now); return;
|
|
}
|
|
QString peer = conn.peer_id ? QStringLiteral("%1").arg(conn.peer_id, 16, 16, QChar('0')) : text("Connecting");
|
|
m_title->setText(QStringLiteral("%1 [%2] · %3").arg(QString::fromUtf8(conn.name), peer, conn.up ? "UP" : "DOWN"));
|
|
QSet<quint64> present{0}; uint64_t inflight = 0;
|
|
bool changed = !m_canvas->series.contains(0);
|
|
addSeries(0, 0, QStringLiteral("CONN"), text("All links: inflight / TX / RX"));
|
|
for (const auto& link : links) {
|
|
present.insert(link.id); inflight += link.inflight;
|
|
changed |= !m_canvas->series.contains(link.id);
|
|
QString local = address(link.description.local), remote = address(link.description.remote);
|
|
QString tooltip = QStringLiteral("%1 · %2\n%3 → %4\n%5").arg(QString::fromUtf8(link.description.protocol),
|
|
QString::fromUtf8(link.description.socket_name), local, remote, link.description.status ? "UP" : "DOWN");
|
|
addSeries(link.id, link.description.id, QStringLiteral("LINK%1").arg(link.description.id), tooltip);
|
|
append(m_canvas->series[link.id], at, link.tx_bytes, link.rx_bytes, link.inflight);
|
|
}
|
|
for (auto it = m_canvas->series.begin(); it != m_canvas->series.end();) {
|
|
if (present.contains(it.key())) { ++it; continue; }
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "traffic graph link removed: connection=%llx generation=%llu",
|
|
(unsigned long long)m_id, (unsigned long long)it.key());
|
|
delete it->button; it = m_canvas->series.erase(it); changed = true;
|
|
}
|
|
append(m_canvas->series[0], at, conn.tx_bytes, conn.rx_bytes, inflight);
|
|
m_canvas->now = at;
|
|
if (changed) arrangeLegend();
|
|
m_canvas->update();
|
|
}
|
|
protected:
|
|
void resizeEvent(QResizeEvent* event) override { QWidget::resizeEvent(event); arrangeLegend(); }
|
|
private:
|
|
// Отдельно хранить поколение: local_link_id циклически переиспользуется транспортом.
|
|
quint64 m_id;
|
|
QLabel* m_title;
|
|
QGridLayout* m_legend;
|
|
TrafficCanvas* m_canvas;
|
|
unsigned m_nextColor = 0;
|
|
|
|
static QString address(const etcp_link_address& value) {
|
|
if (!value.family) return QStringLiteral("—");
|
|
QHostAddress host;
|
|
if (value.family == 4) {
|
|
quint32 ipv4 = (quint32(value.bytes[0]) << 24) | (quint32(value.bytes[1]) << 16) |
|
|
(quint32(value.bytes[2]) << 8) | value.bytes[3];
|
|
host.setAddress(ipv4);
|
|
} else { Q_IPV6ADDR ipv6; memcpy(ipv6.c, value.bytes, 16); host.setAddress(ipv6); }
|
|
return QStringLiteral("[%1]:%2").arg(host.toString()).arg(value.port);
|
|
}
|
|
|
|
void addSeries(quint64 id, unsigned number, const QString& name, const QString& tooltip) {
|
|
auto it = m_canvas->series.find(id);
|
|
if (it == m_canvas->series.end()) {
|
|
TrafficSeries value;
|
|
value.number = number;
|
|
value.color = id ? QColor::fromHsv((35 + m_nextColor++ * 137) % 360, 190, 210) : QColor("#3498db");
|
|
value.button = new QToolButton(this); value.button->setCheckable(true); value.button->setChecked(true);
|
|
value.button->setFixedWidth(90); value.button->setCursor(Qt::PointingHandCursor);
|
|
value.button->setObjectName(QStringLiteral("trafficSeries_%1").arg(id));
|
|
value.button->setStyleSheet(QStringLiteral(
|
|
"QToolButton { border: none; border-radius: 4px; padding: 3px; color: palette(mid); }"
|
|
"QToolButton:checked { color: %1; } QToolButton:hover { background: palette(midlight); }").arg(value.color.name()));
|
|
connect(value.button, &QToolButton::toggled, m_canvas, [this, id](bool enabled) {
|
|
auto& item = m_canvas->series[id]; QFont font = item.button->font(); font.setStrikeOut(!enabled);
|
|
item.button->setFont(font); m_canvas->update();
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "traffic graph series: connection=%llx generation=%llu visible=%d",
|
|
(unsigned long long)m_id, (unsigned long long)id, enabled);
|
|
});
|
|
it = m_canvas->series.insert(id, value);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "traffic graph series added: connection=%llx generation=%llu color=%s",
|
|
(unsigned long long)m_id, (unsigned long long)id, qPrintable(it->color.name()));
|
|
}
|
|
it->button->setText(QStringLiteral("● %1").arg(name)); it->button->setAccessibleName(name); it->button->setToolTip(tooltip);
|
|
}
|
|
|
|
void arrangeLegend() {
|
|
int columns = std::max(1, (width() - 12) / 94), index = 0;
|
|
std::vector<TrafficSeries*> ordered;
|
|
for (auto& item : m_canvas->series) { m_legend->removeWidget(item.button); ordered.push_back(&item); }
|
|
std::sort(ordered.begin(), ordered.end(), [](const auto* a, const auto* b) { return a->number < b->number; });
|
|
for (auto* item : ordered) { m_legend->addWidget(item->button, index / columns, index % columns); ++index; }
|
|
}
|
|
|
|
// Среднее фиксируется при получении; скрытие серии не меняет её историю и вычисление скорости.
|
|
void append(TrafficSeries& series, uint64_t at, uint64_t tx, uint64_t rx, uint64_t flight) {
|
|
auto& history = series.samples;
|
|
if (!history.empty() && at <= history.back().at) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "traffic graph: non-increasing time connection=%llx now=%llu previous=%llu",
|
|
(unsigned long long)m_id, (unsigned long long)at, (unsigned long long)history.back().at); return;
|
|
}
|
|
bool gap = history.empty() || at - history.back().at > MaxGapMs;
|
|
if (!history.empty() && (tx < history.back().tx || rx < history.back().rx)) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "traffic graph: counters regressed connection=%llx TX=%llu RX=%llu",
|
|
(unsigned long long)m_id, (unsigned long long)tx, (unsigned long long)rx); gap = true;
|
|
}
|
|
TrafficSample sample{at, tx, rx, flight, NAN, NAN, gap};
|
|
if (!gap) {
|
|
const TrafficSample* first = nullptr;
|
|
for (auto it = history.rbegin(); it != history.rend(); ++it) {
|
|
if (at - it->at > RateMs) break;
|
|
first = &*it;
|
|
if (it->gap) break;
|
|
}
|
|
if (first) {
|
|
sample.txRate = (tx - first->tx) * 1000.0 / (at - first->at);
|
|
sample.rxRate = (rx - first->rx) * 1000.0 / (at - first->at);
|
|
}
|
|
}
|
|
history.push_back(sample);
|
|
while (history.size() > 256 || at - history.front().at > HistoryMs + RateMs) history.pop_front();
|
|
}
|
|
};
|
|
|
|
/* Каждая ось начинается с нуля и учитывает только включённые серии в видимом временном окне. */
|
|
void TrafficCanvas::paintEvent(QPaintEvent*) {
|
|
QPainter p(this); p.setRenderHint(QPainter::Antialiasing); p.fillRect(rect(), palette().base());
|
|
QColor foreground = palette().text().color(), muted = foreground; muted.setAlphaF(.65);
|
|
auto value = [](const TrafficSample& sample, int row) {
|
|
return row == 0 ? double(sample.inflight) : row == 1 ? sample.txRate : sample.rxRate;
|
|
};
|
|
for (int row = 0; row < 3; ++row) {
|
|
double maximum = 1; bool have = false;
|
|
for (const auto& line : series) if (line.button->isChecked()) for (const auto& sample : line.samples) {
|
|
if (now - sample.at > HistoryMs || !std::isfinite(value(sample, row))) continue;
|
|
maximum = std::max(maximum, value(sample, row)); have = true;
|
|
}
|
|
double power = std::pow(10.0, std::floor(std::log10(maximum))), rounded = maximum / power;
|
|
double top = (rounded <= 1 ? 1 : rounded <= 2 ? 2 : rounded <= 5 ? 5 : 10) * power;
|
|
double factor = top >= 1048576 ? 1048576 : top >= 1024 ? 1024 : 1;
|
|
QString unit = factor == 1048576 ? "MiB" : factor == 1024 ? "KiB" : "B";
|
|
if (row) unit += "/s";
|
|
double y = row * 122.0; QRectF box(78, y + 24, width() - 94, 76);
|
|
p.setPen(foreground);
|
|
p.drawText(QRectF(10, y + 2, width() - 20, 20),
|
|
QStringLiteral("%1 · %2%3").arg(row == 0 ? text("Inflight") : row == 1 ? "TX" : "RX", unit,
|
|
have ? QString() : QStringLiteral(" · ") + text("No samples")));
|
|
auto px = [&](uint64_t at) { return box.right() - double(now - at) / HistoryMs * box.width(); };
|
|
auto py = [&](double v) { return box.bottom() - v / top * box.height(); };
|
|
for (int tick = 0; tick <= 2; ++tick) {
|
|
double v = top * tick / 2, position = py(v); p.setPen(QPen(muted, .4));
|
|
p.drawLine(QPointF(box.left(), position), QPointF(box.right(), position));
|
|
p.setPen(muted); p.drawText(QRectF(0, position - 8, 70, 16), Qt::AlignRight | Qt::AlignVCenter,
|
|
QString::number(v / factor, 'f', factor == 1 ? 0 : 1));
|
|
}
|
|
for (int second = -30; second <= 0; second += 10) {
|
|
double x = box.right() + second / 30.0 * box.width(); p.setPen(QPen(muted, .4));
|
|
p.drawLine(QPointF(x, box.top()), QPointF(x, box.bottom())); p.setPen(muted);
|
|
p.drawText(QRectF(x - 22, box.bottom() + 2, 44, 18), Qt::AlignCenter, QStringLiteral("%1 s").arg(second));
|
|
}
|
|
p.save(); p.setClipRect(box.adjusted(-1, -1, 1, 1));
|
|
for (auto it = series.cbegin(); it != series.cend(); ++it) {
|
|
if (!it->button->isChecked()) continue;
|
|
QPainterPath path; bool started = false;
|
|
for (const auto& sample : it->samples) {
|
|
double v = value(sample, row);
|
|
if (now - sample.at > HistoryMs || !std::isfinite(v)) { started = false; continue; }
|
|
QPointF point(px(sample.at), py(v));
|
|
if (!started || sample.gap) path.moveTo(point); else path.lineTo(point);
|
|
started = true;
|
|
}
|
|
p.setPen(QPen(it->color, it.key() ? 1.5 : 2.5)); p.drawPath(path);
|
|
}
|
|
p.restore();
|
|
}
|
|
}
|
|
|
|
static void networkSnapshot(const uint8_t* data, int len) { if (activeMonitor) activeMonitor->onSnapshot(data, len); }
|
|
|
|
NetworkTrafficWindow* NetworkTrafficWindow::showMonitor() {
|
|
auto* window = activeMonitor;
|
|
if (!window) window = new NetworkTrafficWindow;
|
|
if (window->isMinimized()) window->showNormal(); else window->show();
|
|
window->raise(); window->activateWindow();
|
|
return window;
|
|
}
|
|
|
|
NetworkTrafficWindow::NetworkTrafficWindow() : QDialog(nullptr) {
|
|
setObjectName("networkTrafficWindow"); setWindowTitle(text("Network Traffic")); resize(900, 850); setMinimumWidth(460);
|
|
setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_QuitOnClose, false);
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
auto* layout = new QVBoxLayout(this);
|
|
auto* description = new QLabel(text("Last 30 seconds · TX / RX averaged over 1 second · Click legend to show or hide a series"), this);
|
|
description->setWordWrap(true); layout->addWidget(description);
|
|
auto* scroll = new QScrollArea(this); scroll->setWidgetResizable(true); layout->addWidget(scroll);
|
|
auto* content = new QWidget(scroll); m_rows = new QVBoxLayout(content); m_rows->setAlignment(Qt::AlignTop);
|
|
m_rows->setContentsMargins(0, 0, 0, 0); m_rows->setSpacing(12); scroll->setWidget(content);
|
|
m_empty = new QLabel(text("Waiting for network statistics"), content); m_empty->setWordWrap(true); m_rows->addWidget(m_empty);
|
|
m_timer = new QTimer(this); m_timer->setInterval(250);
|
|
connect(m_timer, &QTimer::timeout, this, &NetworkTrafficWindow::onTimerTick);
|
|
activeMonitor = this; gui_bridge_set_network_stats_cb(networkSnapshot);
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "network traffic window created: history_ms=30000 interval_ms=250 rate_ms=1000");
|
|
}
|
|
|
|
NetworkTrafficWindow::~NetworkTrafficWindow() {
|
|
m_timer->stop();
|
|
if (activeMonitor == this) { activeMonitor = nullptr; gui_bridge_set_network_stats_cb(nullptr); }
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "network traffic window destroyed: connections=%d", int(m_connections.size()));
|
|
}
|
|
void NetworkTrafficWindow::showEvent(QShowEvent* event) {
|
|
QDialog::showEvent(event); m_timer->start(); onTimerTick();
|
|
}
|
|
void NetworkTrafficWindow::hideEvent(QHideEvent* event) {
|
|
m_timer->stop(); m_requestId = 0; QDialog::hideEvent(event);
|
|
}
|
|
void NetworkTrafficWindow::closeEvent(QCloseEvent* event) {
|
|
QDialog::closeEvent(event);
|
|
if (event->isAccepted() && activeMonitor == this) { activeMonitor = nullptr; gui_bridge_set_network_stats_cb(nullptr); }
|
|
}
|
|
void NetworkTrafficWindow::onTimerTick() {
|
|
int result = gui_bridge_request_network_stats(&m_requestId);
|
|
if (result == 0) {
|
|
if (!m_connections.isEmpty()) DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "traffic graphs cleared: core unavailable");
|
|
qDeleteAll(m_connections); m_connections.clear(); m_requestId = 0;
|
|
m_empty->setText(text("Network core unavailable")); m_empty->show();
|
|
} else if (result < 0) { m_empty->setText(text("Unable to collect network statistics")); m_empty->show(); }
|
|
}
|
|
|
|
/* Проверяем native POD и границы всех массивов до изменения существующих графиков. */
|
|
void NetworkTrafficWindow::onSnapshot(const uint8_t* data, int len) {
|
|
if (!data || len < int(sizeof(chat_network_stats_header))) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "traffic graphs: truncated header bytes=%d", len); return;
|
|
}
|
|
chat_network_stats_header header; memcpy(&header, data, sizeof(header));
|
|
if (!isVisible() || header.request_id != m_requestId || !m_requestId) return;
|
|
if (header.failed == 1 && header.count == 0 && len == int(sizeof(header))) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "traffic graphs: collection failed request=%llu", (unsigned long long)header.request_id);
|
|
m_empty->setText(text("Unable to collect network statistics")); m_empty->show(); return;
|
|
}
|
|
struct Connection { chat_network_stats_conn value; std::vector<chat_network_stats_link> links; };
|
|
std::vector<Connection> connections; QSet<quint64> present;
|
|
const uint8_t* p = data + sizeof(header); size_t remaining = len - sizeof(header);
|
|
auto invalid = [&] { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "traffic graphs: invalid snapshot request=%llu count=%u bytes=%d remaining=%zu",
|
|
(unsigned long long)header.request_id, header.count, len, remaining); };
|
|
if (header.failed || !header.at_ms || header.count > remaining / sizeof(chat_network_stats_conn)) { invalid(); return; }
|
|
for (uint32_t i = 0; i < header.count; ++i) {
|
|
if (remaining < sizeof(chat_network_stats_conn)) { invalid(); return; }
|
|
Connection conn; memcpy(&conn.value, p, sizeof(conn.value)); p += sizeof(conn.value); remaining -= sizeof(conn.value);
|
|
if (!conn.value.id || present.contains(conn.value.id) || conn.value.up > 1 ||
|
|
!memchr(conn.value.name, 0, sizeof(conn.value.name)) || conn.value.link_count > remaining / sizeof(chat_network_stats_link)) {
|
|
invalid(); return;
|
|
}
|
|
present.insert(conn.value.id); QSet<quint64> links;
|
|
for (uint32_t j = 0; j < conn.value.link_count; ++j) {
|
|
chat_network_stats_link link; memcpy(&link, p, sizeof(link)); p += sizeof(link); remaining -= sizeof(link);
|
|
if (!link.id || links.contains(link.id) || !link.description.id || link.description.status > 1 ||
|
|
!memchr(link.description.protocol, 0, sizeof(link.description.protocol)) ||
|
|
!memchr(link.description.socket_name, 0, sizeof(link.description.socket_name)) ||
|
|
(link.description.local.family && link.description.local.family != 4 && link.description.local.family != 6) ||
|
|
(link.description.remote.family && link.description.remote.family != 4 && link.description.remote.family != 6)) {
|
|
invalid(); return;
|
|
}
|
|
links.insert(link.id); conn.links.push_back(link);
|
|
}
|
|
connections.push_back(std::move(conn));
|
|
}
|
|
if (remaining) { invalid(); return; }
|
|
for (auto it = m_connections.begin(); it != m_connections.end();) {
|
|
if (present.contains(it.key())) { ++it; continue; }
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "traffic graph connection removed: id=%llx", (unsigned long long)it.key());
|
|
delete it.value(); it = m_connections.erase(it);
|
|
}
|
|
for (const auto& conn : connections) {
|
|
auto* panel = m_connections.value(conn.value.id);
|
|
if (!panel) {
|
|
panel = new TrafficConnection(conn.value.id, this); m_rows->addWidget(panel); m_connections.insert(conn.value.id, panel);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "traffic graph connection added: id=%llx peer=%016llx links=%u",
|
|
(unsigned long long)conn.value.id, (unsigned long long)conn.value.peer_id, conn.value.link_count);
|
|
}
|
|
panel->apply(conn.value, conn.links, header.at_ms);
|
|
}
|
|
m_empty->setText(text("No ETCP connections")); m_empty->setVisible(connections.empty());
|
|
}
|