From 34794f80a2fa64fe5d17685a93a4122eecb25395 Mon Sep 17 00:00:00 2001 From: R E Broadley Date: Wed, 2 Apr 2025 14:43:59 +0100 Subject: [PATCH 1/3] qt: Add formatBytesps function for bytes per second display Add a new GUI utility function to format bytes per second values with appropriate units (B/s, kB/s, MB/s, GB/s) and precision, ensuring consistent and readable display of network traffic rates in the traffic graph widget. --- src/qt/guiutil.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index c1bf5a56032..26568e1ba2e 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -833,6 +833,40 @@ QString formatBytes(uint64_t bytes) return QObject::tr("%1 GB").arg(bytes / 1'000'000'000); } +QString formatBytesps(float val) +{ + if (val < 10) + //: "Bytes per second" + return QObject::tr("%1 B/s").arg(0.01 * int(val * 100 + 0.5)); + if (val < 100) + //: "Bytes per second" + return QObject::tr("%1 B/s").arg(0.1 * int(val * 10 + 0.5)); + if (val < 1'000) + //: "Bytes per second" + return QObject::tr("%1 B/s").arg(int(val + 0.5)); + if (val < 10'000) + //: "Kilobytes per second" + return QObject::tr("%1 kB/s").arg(0.01 * int(val / 10 + 0.5)); + if (val < 100'000) + //: "Kilobytes per second" + return QObject::tr("%1 kB/s").arg(0.1 * int(val / 100 + 0.5)); + if (val < 1'000'000) + //: "Kilobytes per second" + return QObject::tr("%1 kB/s").arg(int(val / 1'000 + 0.5)); + if (val < 10'000'000) + //: "Megabytes per second" + return QObject::tr("%1 MB/s").arg(0.01 * int(val / 10'000 + 0.5)); + if (val < 100'000'000) + //: "Megabytes per second" + return QObject::tr("%1 MB/s").arg(0.1 * int(val / 100'000 + 0.5)); + if (val < 10'000'000'000) + //: "Megabytes per second" + return QObject::tr("%1 MB/s").arg(long(val / 1'000'000 + 0.5)); + + //: "Gigabytes per second" + return QObject::tr("%1 GB/s").arg(long(val / 1'000'000'000 + 0.5)); +} + qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize, qreal font_size) { while(font_size >= minPointSize) { font.setPointSizeF(font_size); From 2158ab7dc9025851b1d447271fe0175291a00fbe Mon Sep 17 00:00:00 2001 From: R E Broadley Date: Wed, 2 Apr 2025 16:50:26 +0100 Subject: [PATCH 2/3] util: Add FormatISO8601Time function for time-only ISO format Add a new time formatting function that matches the existing date and datetime ISO8601 formatting functions, providing consistent time display for the traffic graph widget tooltips and other UI elements that need time-only display. --- src/util/time.cpp | 8 ++++++++ src/util/time.h | 1 + 2 files changed, 9 insertions(+) diff --git a/src/util/time.cpp b/src/util/time.cpp index cafc27e0d05..5507caff972 100644 --- a/src/util/time.cpp +++ b/src/util/time.cpp @@ -75,6 +75,14 @@ void MockableSteadyClock::ClearMockTime() int64_t GetTime() { return GetTime().count(); } +std::string FormatISO8601Time(int64_t nTime) +{ + const std::chrono::sys_seconds secs{std::chrono::seconds{nTime}}; + const auto days{std::chrono::floor(secs)}; + const std::chrono::hh_mm_ss hms{secs - days}; + return strprintf("%02i:%02i:%02iZ", hms.hours().count(), hms.minutes().count(), hms.seconds().count()); +} + std::string FormatISO8601DateTime(int64_t nTime) { const std::chrono::sys_seconds secs{std::chrono::seconds{nTime}}; diff --git a/src/util/time.h b/src/util/time.h index c43b306ff24..ef7e8ac00cb 100644 --- a/src/util/time.h +++ b/src/util/time.h @@ -130,6 +130,7 @@ T GetTime() * ISO 8601 formatting is preferred. Use the FormatISO8601{DateTime,Date} * helper functions if possible. */ +std::string FormatISO8601Time(int64_t nTime); std::string FormatISO8601DateTime(int64_t nTime); std::string FormatISO8601Date(int64_t nTime); std::optional ParseISO8601DateTime(std::string_view str); From bc13d019838fa19dd0b4bd6846419e382a6b6505 Mon Sep 17 00:00:00 2001 From: R E Broadley Date: Wed, 2 Apr 2025 13:21:18 +0100 Subject: [PATCH 3/3] qt: Enhance TrafficGraphWidget with multi-timeframe support and data persistence This commit significantly improves the network traffic graph widget with: 1. Multiple timeframe support - View traffic data across different time periods (5 minutes to 28 days) using an enhanced slider interface 2. Traffic data persistence - Save and restore traffic information between sessions, preserving historical traffic patterns 3. Interactive visualization features: - Logarithmic scale toggle (mouse click) for better visualization of varying traffic volumes - Interactive tooltips showing detailed traffic information at specific points - Yellow highlight indicators for selected data points 4. Smooth transitions between different time ranges with animated scaling These improvements allow users to better monitor and analyze network traffic patterns over time, which is especially useful for debugging connectivity issues or understanding network behavior under different conditions. The implementation includes proper thread-safety considerations and handles edge cases like time jumps or missing data appropriately. --- src/qt/forms/debugwindow.ui | 27 +- src/qt/guiutil.h | 1 + src/qt/rpcconsole.cpp | 41 ++- src/qt/rpcconsole.h | 9 +- src/qt/trafficgraphwidget.cpp | 658 +++++++++++++++++++++++++++++----- src/qt/trafficgraphwidget.h | 72 +++- 6 files changed, 674 insertions(+), 134 deletions(-) diff --git a/src/qt/forms/debugwindow.ui b/src/qt/forms/debugwindow.ui index eccea143189..7272aeeba48 100644 --- a/src/qt/forms/debugwindow.ui +++ b/src/qt/forms/debugwindow.ui @@ -665,20 +665,29 @@ - 1 + 0 - 288 + 2400 + + + 200 - 12 + 200 - 6 + 0 Qt::Horizontal + + QSlider::TicksBelow + + + 200 + @@ -694,16 +703,6 @@ - - - - &Reset - - - false - - - diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h index 1b493430731..43b558c5d88 100644 --- a/src/qt/guiutil.h +++ b/src/qt/guiutil.h @@ -246,6 +246,7 @@ namespace GUIUtil QString formatNiceTimeOffset(qint64 secs); QString formatBytes(uint64_t bytes); + QString formatBytesps(float bytes); qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize = 4, qreal startPointSize = 14); diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index 9d7c17ac911..9b7f4ff7cc7 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -53,7 +53,6 @@ using util::Join; const int CONSOLE_HISTORY = 50; -const int INITIAL_TRAFFIC_GRAPH_MINS = 30; const QSize FONT_RANGE(4, 40); const char fontSizeSettingsKey[] = "consoleFontSize"; @@ -566,7 +565,6 @@ RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformSty connect(ui->clearButton, &QAbstractButton::clicked, [this] { clear(); }); connect(ui->fontBiggerButton, &QAbstractButton::clicked, this, &RPCConsole::fontBigger); connect(ui->fontSmallerButton, &QAbstractButton::clicked, this, &RPCConsole::fontSmaller); - connect(ui->btnClearTrafficGraph, &QPushButton::clicked, ui->trafficGraph, &TrafficGraphWidget::clear); // disable the wallet selector by default ui->WalletSelector->setVisible(false); @@ -578,7 +576,7 @@ RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformSty // based timer interface m_node.rpcSetTimerInterfaceIfUnset(rpcTimerInterface); - setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS); + setTrafficGraphRange(1); // 1 is the lowest setting (0 bumps up) updateDetailWidget(); consoleFontSize = settings.value(fontSizeSettingsKey, QFont().pointSize()).toInt(); @@ -1166,21 +1164,44 @@ void RPCConsole::scrollToEnd() void RPCConsole::on_sldGraphRange_valueChanged(int value) { - const int multiplier = 5; // each position on the slider represents 5 min - int mins = value * multiplier; - setTrafficGraphRange(mins); + setTrafficGraphRange((value + 100) / 200 + 1); } -void RPCConsole::setTrafficGraphRange(int mins) +void RPCConsole::setTrafficGraphRange(int value) { - ui->trafficGraph->setGraphRange(std::chrono::minutes{mins}); + int mins = ui->trafficGraph->setGraphRange(value); + if (value) + m_set_slider_value = (value - 1) * 200; + else { + // When bumping, calculate the proper slider position based on the traffic graph's new value + unsigned int new_graph_value = ui->trafficGraph->getCurrentRangeIndex() + 1; // +1 because the index is 0-based + m_set_slider_value = (new_graph_value - 1) * 200; + ui->sldGraphRange->blockSignals(true); + ui->sldGraphRange->setValue(m_set_slider_value); + ui->sldGraphRange->blockSignals(false); + } ui->lblGraphRange->setText(GUIUtil::formatDurationStr(std::chrono::minutes{mins})); } +void RPCConsole::on_sldGraphRange_sliderReleased() +{ + ui->sldGraphRange->setValue(m_set_slider_value); + m_slider_in_use = false; +} + +void RPCConsole::on_sldGraphRange_sliderPressed() { m_slider_in_use = true; } + void RPCConsole::updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut) { - ui->lblBytesIn->setText(GUIUtil::formatBytes(totalBytesIn)); - ui->lblBytesOut->setText(GUIUtil::formatBytes(totalBytesOut)); + if (!m_slider_in_use && ui->trafficGraph->graphRangeBump()) + setTrafficGraphRange(0); // bump it up + + // Add baseline values to the current node values + quint64 totalIn = totalBytesIn + ui->trafficGraph->getBaselineBytesRecv(); + quint64 totalOut = totalBytesOut + ui->trafficGraph->getBaselineBytesSent(); + + ui->lblBytesIn->setText(GUIUtil::formatBytes(totalIn)); + ui->lblBytesOut->setText(GUIUtil::formatBytes(totalOut)); } void RPCConsole::updateDetailWidget() diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index 894ecb1fdf5..a75813f7f57 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -90,6 +90,8 @@ private Q_SLOTS: void on_openDebugLogfileButton_clicked(); /** change the time range of the network traffic graph */ void on_sldGraphRange_valueChanged(int value); + void on_sldGraphRange_sliderReleased(); + void on_sldGraphRange_sliderPressed(); /** update traffic statistics */ void updateTrafficStats(quint64 totalBytesIn, quint64 totalBytesOut); void resizeEvent(QResizeEvent *event) override; @@ -146,10 +148,9 @@ public Q_SLOTS: } const ts; void startExecutor(); - void setTrafficGraphRange(int mins); + void setTrafficGraphRange(int value); - enum ColumnWidths - { + enum ColumnWidths { ADDRESS_COLUMN_WIDTH = 200, SUBVERSION_COLUMN_WIDTH = 150, PING_COLUMN_WIDTH = 80, @@ -177,6 +178,8 @@ public Q_SLOTS: bool m_is_executing{false}; QByteArray m_peer_widget_header_state; QByteArray m_banlist_widget_header_state; + bool m_slider_in_use{false}; + int m_set_slider_value{0}; /** Update UI with latest network info from model. */ void updateNetworkState(); diff --git a/src/qt/trafficgraphwidget.cpp b/src/qt/trafficgraphwidget.cpp index fb6f2cb4642..ccbd809b5f1 100644 --- a/src/qt/trafficgraphwidget.cpp +++ b/src/qt/trafficgraphwidget.cpp @@ -5,12 +5,13 @@ #include #include #include +#include #include #include #include #include - +#include #include #include @@ -20,152 +21,631 @@ #define YMARGIN 10 TrafficGraphWidget::TrafficGraphWidget(QWidget* parent) - : QWidget(parent), - vSamplesIn(), - vSamplesOut() + : QWidget(parent) { - timer = new QTimer(this); - connect(timer, &QTimer::timeout, this, &TrafficGraphWidget::updateRates); + m_timer = new QTimer(this); + connect(m_timer, &QTimer::timeout, this, &TrafficGraphWidget::updateStuff); + m_timer->setInterval(75); + m_timer->start(); + setMouseTracking(true); } void TrafficGraphWidget::setClientModel(ClientModel *model) { - clientModel = model; + m_client_model = model; if(model) { - nLastBytesIn = model->node().getTotalBytesRecv(); - nLastBytesOut = model->node().getTotalBytesSent(); + m_data_dir = model->dataDir().toStdString(); + m_node = &model->node(); // Cache the node interface + + if (m_samples_in[0].empty() && m_samples_out[0].empty()) { + loadData(); + } + } else { + // Save data when model is being disconnected during shutdown + saveData(); } } -std::chrono::minutes TrafficGraphWidget::getGraphRange() const { return m_range; } +int TrafficGraphWidget::yValue(float value) const +{ + int h = height() - YMARGIN * 2; + return YMARGIN + h - (h * 1.0 * (m_toggle ? (std::pow(value, 0.30102) / std::pow(m_fmax, 0.30102)) : (value / m_fmax))); +} -void TrafficGraphWidget::paintPath(QPainterPath &path, QQueue &samples) +int TrafficGraphWidget::paintPath(QPainterPath& path, const QQueue& samples) { - int sampleCount = samples.size(); - if(sampleCount > 0) { - int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2; - int x = XMARGIN + w; - path.moveTo(x, YMARGIN + h); - for(int i = 0; i < sampleCount; ++i) { - x = XMARGIN + w - w * i / DESIRED_SAMPLES; - int y = YMARGIN + h - (int)(h * samples.at(i) / fMax); - path.lineTo(x, y); + int sample_count = std::min(int(DESIRED_SAMPLES * m_range / m_values[m_value]), int(samples.size())) - 1; + if (sample_count <= 0) return 0; + int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2; + int x = XMARGIN + w, i; + path.moveTo(x + 1, YMARGIN + h); // Overscan by 1 pixel to hide bright line + for (i = 0; i <= sample_count; ++i) { + if (i < 1) path.lineTo(x + 1, yValue(samples.at(0))); // Overscan by 1 pixel to the right + double ratio = static_cast(i) * m_values[m_value] / m_range / (DESIRED_SAMPLES - 1); + x = XMARGIN + static_cast(w - w * ratio + 0.5); + if (i == sample_count && (sample_count < samples.size() - 1 || samples.size() >= DESIRED_SAMPLES)) { + path.lineTo(x, yValue(samples.at(i))); + x = XMARGIN - 1; // Overscan by one pixel to the left } - path.lineTo(x, YMARGIN + h); + path.lineTo(x, yValue(samples.at(i))); + } + path.lineTo(x, YMARGIN + h); + + return x; +} + +void TrafficGraphWidget::focusSlider() +{ + QWidget* parent = parentWidget(); + if (parent) { + QSlider* slider = parent->findChild("sldGraphRange"); + if (slider) slider->setFocus(Qt::OtherFocusReason); + } +} + +void TrafficGraphWidget::mousePressEvent(QMouseEvent* event) +{ + QWidget::mousePressEvent(event); + focusSlider(); + m_toggle = !m_toggle; + m_update = true; + update(); +} + +void TrafficGraphWidget::leaveEvent(QEvent* event) +{ + QWidget::leaveEvent(event); + if (!m_tt_point) return; + m_tt_point = 0; + m_update = true; + update(); +} + +void TrafficGraphWidget::mouseMoveEvent(QMouseEvent* event) +{ + QWidget::mouseMoveEvent(event); + static int last_x = -1, last_y = -1; + QPointF pos = event->position(); + QPointF globalPos = event->globalPosition(); + int x = qRound(pos.x()), y = qRound(pos.y()); + m_x_offset = qRound(globalPos.x()) - x; + m_y_offset = qRound(globalPos.y()) - y; + if (last_x == x && last_y == y) return; // Do nothing if mouse hasn't moved + int w = width() - XMARGIN * 2; + int i = (w + XMARGIN - x) * (DESIRED_SAMPLES - 1) / w, closest_i = 0; + int sampleSize = m_time_stamp[m_value].size(); + unsigned int smallest_distance = 50; + bool is_in_series = true; + for (int test_i = std::max(i - 3, 0); test_i < std::min(i + 9, sampleSize); test_i++) { + float in_val = m_samples_in[m_value].at(test_i), out_val = m_samples_out[m_value].at(test_i); + int y_in = yValue(in_val), y_out = yValue(out_val); + unsigned int distance_in = abs(y - y_in), distance_out = abs(y - y_out); + unsigned int min_distance = std::min(distance_in, distance_out) + abs(test_i - i); + if (min_distance < smallest_distance) { + smallest_distance = min_distance; + closest_i = test_i + 1; + is_in_series = (distance_in < distance_out); + } + } + if (m_tt_point != closest_i || m_tt_in_series != is_in_series) { + m_tt_point = closest_i; + m_tt_in_series = is_in_series; + m_update = true; + update(); // Calls paintEvent() to draw or delete the highlighted point + } + last_x = x; + last_y = y; +} + +void TrafficGraphWidget::drawTooltipPoint(QPainter& painter) +{ + int w = width() - XMARGIN * 2; + double ratio = static_cast(m_tt_point-1) * m_values[m_value] / m_range / (DESIRED_SAMPLES-1); + int x = XMARGIN + static_cast(w - w * ratio + 0.5); + float in_sample = m_samples_in[m_value].at(m_tt_point-1); + float out_sample = m_samples_out[m_value].at(m_tt_point-1); + float selected_sample = m_tt_in_series ? in_sample : out_sample; + int y = yValue(selected_sample); + painter.setPen(Qt::yellow); + painter.drawEllipse(QPointF(x, y), 3, 3); + QString str_tt; + int64_t sample_time = 0; + if (m_tt_point < m_time_stamp[m_value].size()) + sample_time = m_time_stamp[m_value].at(m_tt_point); + if (!sample_time) // Either the oldest sample or the first ever sample + sample_time = m_time_stamp[m_value].at(m_tt_point - 1); + int age = TicksSinceEpoch(SystemClock::now()) - sample_time / 1000; + if (age < 60 * 60 * 23) + str_tt += QString::fromStdString(FormatISO8601Time(sample_time / 1000)); + else + str_tt += QString::fromStdString(FormatISO8601DateTime(sample_time / 1000)); + int duration = (m_time_stamp[m_value].at(m_tt_point - 1) - sample_time); + if (duration > 0) { + if (duration > 9999) + str_tt += " +" + GUIUtil::formatDurationStr(std::chrono::seconds{(duration + 500) / 1000}); + else + str_tt += " +" + GUIUtil::formatPingTime(std::chrono::microseconds{duration * 1000}); + } + str_tt += "\n " + tr("In") + " " + GUIUtil::formatBytesps(m_samples_in[m_value].at(m_tt_point-1) * 1000) + + "\n" + tr("Out") + " " + GUIUtil::formatBytesps(m_samples_out[m_value].at(m_tt_point-1) * 1000); + + // Line below allows ToolTip to move faster than the default ToolTip timeout (10 seconds). + QToolTip::showText(QPoint(x + m_x_offset, y + m_y_offset), str_tt + "."); + QToolTip::showText(QPoint(x + m_x_offset, y + m_y_offset), str_tt); + m_tt_time = GetTime(); +} + +// Helper function to draw text with outline +void DrawOutlinedText(QPainter& painter, int y, const QString& text, int opacity) +{ + // Draw the outline by drawing the text multiple times with small offsets + if (opacity) { + painter.setPen(Qt::black); + for (int dx = -1; dx <= 1; dx++) + for (int dy = -1; dy <= 1; dy++) + if (dx != 0 || dy != 0) + painter.drawText(XMARGIN + dx, y + dy - 2, text); } + + // Draw the main text + painter.setPen(Qt::white); + painter.drawText(XMARGIN, y - 2, text); } void TrafficGraphWidget::paintEvent(QPaintEvent *) { + m_update = false; QPainter painter(this); + int hgt = height(), wid = width(); painter.fillRect(rect(), Qt::black); - if(fMax <= 0.0f) return; - - QColor axisCol(Qt::gray); - int h = height() - YMARGIN * 2; - painter.setPen(axisCol); - painter.drawLine(XMARGIN, YMARGIN + h, width() - XMARGIN, YMARGIN + h); - // decide what order of magnitude we are - int base = std::floor(std::log10(fMax)); - float val = std::pow(10.0f, base); - - const QString units = tr("kB/s"); - const float yMarginText = 2.0; + int base = std::floor(std::log10(m_fmax)); + float val = std::pow(10.0f, base); // kB/s // draw lines + QColor axisCol(Qt::gray); painter.setPen(axisCol); - painter.drawText(XMARGIN, YMARGIN + h - h * val / fMax-yMarginText, QString("%1 %2").arg(val).arg(units)); - for(float y = val; y < fMax; y += val) { - int yy = YMARGIN + h - h * y / fMax; - painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); - } - // if we drew 3 or fewer lines, break them up at the next lower order of magnitude - if(fMax / val <= 3.0f) { - axisCol = axisCol.darker(); - val = pow(10.0f, base - 1); - painter.setPen(axisCol); - painter.drawText(XMARGIN, YMARGIN + h - h * val / fMax-yMarginText, QString("%1 %2").arg(val).arg(units)); + for(float y = val; y < m_fmax; y += val) { + int yy = yValue(y); + painter.drawLine(XMARGIN, yy, wid - XMARGIN, yy); + } + + // if we drew 10 (or 3 when toggles) or fewer lines, break them up at the next lower order of magnitude + if (m_fmax / val <= (m_toggle ? 10.0f : 3.0f)) { + val /= 10; + painter.setPen(axisCol.darker()); int count = 1; - for(float y = val; y < fMax; y += val, count++) { + for (float y = val; y < (!m_toggle || m_fmax / val < 20 ? m_fmax : val*10); y += val, count++) { // don't overwrite lines drawn above - if(count % 10 == 0) - continue; - int yy = YMARGIN + h - h * y / fMax; - painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); + if (count % 10 == 0) continue; + int yy = yValue(y); + painter.drawLine(XMARGIN, yy, wid - XMARGIN, yy); + } + if (m_toggle) { + int yy = yValue(val * 0.1); + painter.setPen(axisCol.darker().darker()); + painter.drawLine(XMARGIN, yy, wid - XMARGIN, yy); } } painter.setRenderHint(QPainter::Antialiasing); - if(!vSamplesIn.empty()) { + if (m_samples_in[m_value].size()) { QPainterPath p; - paintPath(p, vSamplesIn); + paintPath(p, m_samples_in[m_value]); painter.fillPath(p, QColor(0, 255, 0, 128)); painter.setPen(Qt::green); painter.drawPath(p); } - if(!vSamplesOut.empty()) { + int x = 0; + if (m_samples_out[m_value].size()) { QPainterPath p; - paintPath(p, vSamplesOut); + x = paintPath(p, m_samples_out[m_value]); painter.fillPath(p, QColor(255, 0, 0, 128)); painter.setPen(Qt::red); painter.drawPath(p); } + + // Draw black bars and lines to mask the overscanned edges of the graph + painter.fillRect(0, 0, XMARGIN - 1, hgt, Qt::black); + painter.fillRect(wid - XMARGIN + 1, 0, XMARGIN, hgt, Qt::black); + painter.setPen(Qt::black); + painter.drawLine(XMARGIN - 1, 0, XMARGIN - 1, hgt); // Antialiased lines to create some blur + painter.drawLine(wid - XMARGIN + 1, 0, wid - XMARGIN + 1, hgt); + + // Draw the bottom axis line after the graph + painter.setPen(axisCol); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.drawLine(XMARGIN, hgt - YMARGIN, wid - XMARGIN, hgt - YMARGIN); + + int opacity = 0; // Opacity of the black outline around the text + if (x < 70) opacity = 255; + // Draw outlined text for speed labels + DrawOutlinedText(painter, yValue(val*10), GUIUtil::formatBytesps(val * 10000), opacity); + DrawOutlinedText(painter, yValue(val), GUIUtil::formatBytesps(val * 1000), opacity); + if (m_toggle) DrawOutlinedText(painter, yValue(val/10), GUIUtil::formatBytesps(val * 100), opacity); + + if (m_tt_point && m_tt_point <= m_time_stamp[m_value].size()) drawTooltipPoint(painter); + else QToolTip::hideText(); } -void TrafficGraphWidget::updateRates() +void TrafficGraphWidget::updateFmax() { - if(!clientModel) return; + float tmax = 0.0f; + for (const float f : m_samples_in[m_new_value]) + if (f > tmax) tmax = f; + for (const float f : m_samples_out[m_new_value]) + if (f > tmax) tmax = f; + m_new_fmax = std::max(tmax, 0.0001f); +} + +/** + * Smoothly updates a value with acceleration/deceleration for animation. + * + * @param target The target value to approach + * @param current The current value that will be updated + * @param increment The current rate of change (velocity), updated by this function + * @param length The scale factor for controlling animation speed + * @return true if the value was updated, false otherwise + * + * This implements a simple physics-based approach to animation: + * - If moving too slowly, accelerate + * - If moving too quickly, decelerate + * - If close enough to target, snap to it + */ +bool UpdateNum(float target, float& current, float& increment, int length) +{ + if (current == target) return false; - quint64 bytesIn = clientModel->node().getTotalBytesRecv(), - bytesOut = clientModel->node().getTotalBytesSent(); - float in_rate_kilobytes_per_sec = static_cast(bytesIn - nLastBytesIn) / timer->interval(); - float out_rate_kilobytes_per_sec = static_cast(bytesOut - nLastBytesOut) / timer->interval(); - vSamplesIn.push_front(in_rate_kilobytes_per_sec); - vSamplesOut.push_front(out_rate_kilobytes_per_sec); - nLastBytesIn = bytesIn; - nLastBytesOut = bytesOut; + const float threshold = abs(0.8f * current) / length; + const float diff = target - current; - while(vSamplesIn.size() > DESIRED_SAMPLES) { - vSamplesIn.pop_back(); + // Initialize or adjust increment based on current state + if (abs(increment) <= threshold) { // allow equal to as current and increment could be zero + increment = ((current + 1) * (diff > 0 ? 1.0f : -1.0f)) / length; // +1s are to get it started even if current is zero + if (abs(increment) > abs(diff)) { // Only check this when creating an increment + increment = 0; // We have arrived at the target + current = target; + return true; + } + } else { + // Adjust increment based on distance to target + if ((increment > 0 && current + increment * 2 > target) || + (increment < 0 && current + increment * 2 < target)) { + increment *= 0.5f; + } else if ((increment > 0 && current + increment * 8 < target) || + (increment < 0 && current + increment * 8 > target)) { + increment *= 2.0f; + } } - while(vSamplesOut.size() > DESIRED_SAMPLES) { - vSamplesOut.pop_back(); + + // Update current value if increment is significant + if (abs(increment) >= threshold) { + current += increment; + } else if ((increment >= 0 && target > current) || (increment <= 0 && target < current)) { + current = target; + increment = 0; } - float tmax = 0.0f; - for (const float f : vSamplesIn) { - if(f > tmax) tmax = f; + // Ensure minimum value for graph display + if (current <= 0.0f) current = 0.0001f; + + return true; +} + +void TrafficGraphWidget::updateStuff() +{ + if (!m_client_model) return; + + int64_t expected_gap = m_timer->interval(); + int64_t now = TicksSinceEpoch(SystemClock::now()); + bool latest_bytes = false; + quint64 bytes_in = 0, bytes_out = 0; + + // Check for new sample and update display if a new sample is taken for current range + for (int i = 0; i < VALUES_SIZE; i++) { + int64_t msecs_per_sample = static_cast(m_values[i]) * 60000 / DESIRED_SAMPLES; + if (now > (m_last_time[i] + msecs_per_sample - expected_gap / 2)) { + if (!latest_bytes) { + latest_bytes = true; + bytes_in = m_client_model->node().getTotalBytesRecv() + m_baseline_bytes_recv; + bytes_out = m_client_model->node().getTotalBytesSent() + m_baseline_bytes_sent; + } + updateRates(i, now, bytes_in, bytes_out); + if (i == m_value) { + if (m_tt_point && m_tt_point <= DESIRED_SAMPLES) { + m_tt_point++; // Move the selected point to the left + if (m_tt_point > DESIRED_SAMPLES) m_tt_point = 0; + } + m_update = true; + } + if (i == m_new_value) updateFmax(); + } + } + + // Update display due to transition between ranges or new fmax + static float y_increment = 0, x_increment = 0; + if (UpdateNum(m_new_fmax, m_fmax, y_increment, 300)) m_update = true; + int next_m_value = m_value; + if (UpdateNum(m_values[m_new_value], m_range, x_increment, 500)) { + m_update = true; + if (m_values[m_new_value] > m_range && m_values[m_value] < m_range) { + next_m_value = m_value + 1; + } else if (m_new_value < m_value && m_values[m_value - 1] > m_range * 0.99) + next_m_value = m_value - 1; + } else if (m_value != m_new_value) { + m_update = true; + next_m_value = m_new_value; } - for (const float f : vSamplesOut) { - if(f > tmax) tmax = f; + + if (next_m_value != m_value) { + m_tt_point = findClosestPointByTimestamp(next_m_value); + m_value = next_m_value; + } + + static bool last_m_toggle = m_toggle; + if (!QToolTip::isVisible()) { + if (m_tt_point) { // Remove the yellow circle if the ToolTip has gone due to mouse moving elsewhere. + if (last_m_toggle == m_toggle) m_tt_point = 0; + else last_m_toggle = m_toggle; + m_update = true; + } + } else if (m_tt_point && GetTime() >= m_tt_time + 9) m_update = true; + + if (m_update) update(); + static bool graph_visible = false; + if (isVisible() && !window()->isMinimized()) { + if (!graph_visible) focusSlider(); + graph_visible = true; + } else graph_visible = false; +} + +void TrafficGraphWidget::updateRates(int i, int64_t now, quint64 bytes_in, quint64 bytes_out) +{ + int64_t actual_gap = now - m_last_time[i]; + float in_rate_kilobytes_per_msec = static_cast(bytes_in - m_last_bytes_in[i]) / actual_gap; + float out_rate_kilobytes_per_msec = static_cast(bytes_out - m_last_bytes_out[i]) / actual_gap; + m_samples_in[i].push_front(in_rate_kilobytes_per_msec); + m_samples_out[i].push_front(out_rate_kilobytes_per_msec); + m_time_stamp[i].push_front(now); + m_last_bytes_in[i] = bytes_in; + m_last_bytes_out[i] = bytes_out; + m_last_time[i] = now; + static int8_t full[VALUES_SIZE] = {}; + if (full[i] == 0 && m_time_stamp[i].size() <= DESIRED_SAMPLES) full[i] = -1; + while (m_time_stamp[i].size() > DESIRED_SAMPLES) { + if (m_value == i && i < VALUES_SIZE - 1 && full[i] < 0) m_bump = true; + full[i] = 1; + m_samples_in[i].pop_back(); + m_samples_out[i].pop_back(); + m_time_stamp[i].pop_back(); + } +} + +int TrafficGraphWidget::setGraphRange(int value) +{ + // value is the array marker plus 1 (as zero is reserved for bumping up) + if (!value) { // bump + m_bump = false; // Clear the bump flag + value = m_value + 1; + } else + value--; // get the array marker + int old_value = m_new_value; + m_new_value = std::min(value, VALUES_SIZE - 1); + if (m_new_value != old_value) updateFmax(); + + return m_values[m_new_value]; +} + +void TrafficGraphWidget::saveData() +{ + if (m_time_stamp[0].empty() || m_data_dir.empty()) return; + try { + fs::path pathTrafficGraph = fs::path(m_data_dir.c_str()) / "trafficgraph.dat"; + FILE* file = fsbridge::fopen(pathTrafficGraph, "wb"); + if (!file) { + LogPrintf("TrafficGraphWidget: Failed to open file for writing: %s\n", pathTrafficGraph.generic_string()); + throw std::runtime_error("Failed to open file"); + } + AutoFile fileout(file); + if (fileout.IsNull()) throw std::runtime_error("File stream is null"); + fileout << static_cast(1); // Version 1 + + // Get current node values and add them to our baseline + if (m_node) { + m_baseline_bytes_recv += m_node->getTotalBytesRecv(); + m_baseline_bytes_sent += m_node->getTotalBytesSent(); + } + + fileout << VARINT(m_baseline_bytes_recv) << VARINT(m_baseline_bytes_sent); + + for (unsigned int i = 0; i < VALUES_SIZE; i++) { + fileout << VARINT(m_last_bytes_in[i]) << VARINT(m_last_bytes_out[i]); + + fileout << VARINT(static_cast(m_time_stamp[i].size())); + + for (int j = 0; j < m_time_stamp[i].size(); j++) { + fileout << static_cast(m_time_stamp[i].at(j)); + } + + for (int j = 0; j < m_samples_in[i].size(); j++) { + float value = m_samples_in[i].at(j); + uint32_t uint_value; + memcpy(&uint_value, &value, sizeof(float)); // IEEE 754 + fileout << uint_value; + } + + for (int j = 0; j < m_samples_out[i].size(); j++) { + float value = m_samples_out[i].at(j); + uint32_t uint_value; + memcpy(&uint_value, &value, sizeof(float)); // IEEE 754 + fileout << uint_value; + } + } + + fileout.fclose(); + LogPrintf("TrafficGraphWidget: Successfully saved traffic graph data to %s\n", pathTrafficGraph.generic_string()); + } catch (const std::exception& e) { + LogPrintf("TrafficGraphWidget: Error saving data: %s (path: %s)\n", + e.what(), m_data_dir); + } +} + +bool TrafficGraphWidget::loadDataFromBinary() +{ + try { + fs::path pathTrafficGraph = fs::path(m_data_dir.c_str()) / "trafficgraph.dat"; + LogPrintf("TrafficGraphWidget: Attempting to load data from %s\n", pathTrafficGraph.generic_string()); + + FILE* file = fsbridge::fopen(pathTrafficGraph, "rb"); + if (!file) { + LogPrintf("TrafficGraphWidget: File not found or could not be opened\n"); + return false; + } + AutoFile filein(file); + if (filein.IsNull()) return false; + + int version; + filein >> version; + if (version < 1 || version > 1) return false; + + filein >> VARINT(m_baseline_bytes_recv) >> VARINT(m_baseline_bytes_sent); + + uint64_t current_time = TicksSinceEpoch(SystemClock::now()); + + for (unsigned int i = 0; i < VALUES_SIZE; i++) { + filein >> VARINT(m_last_bytes_in[i]) >> VARINT(m_last_bytes_out[i]); + + uint16_t samplesSize; + filein >> VARINT(samplesSize); + + for (unsigned int j = 0; j < samplesSize; j++) { + static uint64_t last_time_ms; + uint64_t time_ms; + filein >> time_ms; + if (!j) m_last_time[i] = last_time_ms = time_ms; + if (time_ms > last_time_ms || time_ms > current_time) return false; // Abort load if data invalid or in future + m_time_stamp[i].push_back(static_cast(time_ms)); + last_time_ms = time_ms; + } + + for (unsigned int j = 0; j < samplesSize; j++) { + uint32_t uint_value; + filein >> uint_value; + float value; + memcpy(&value, &uint_value, sizeof(float)); + m_samples_in[i].push_back(value); + } + + for (unsigned int j = 0; j < samplesSize; j++) { + uint32_t uint_value; + filein >> uint_value; + float value; + memcpy(&value, &uint_value, sizeof(float)); + m_samples_out[i].push_back(value); + } + } + filein.fclose(); + return true; + + } catch (const std::exception& e) { + LogPrintf("TrafficGraphWidget: Error loading data: %s\n", e.what()); + return false; } - fMax = tmax; - update(); } -void TrafficGraphWidget::setGraphRange(std::chrono::minutes new_range) +bool TrafficGraphWidget::loadData() { - m_range = new_range; - const auto msecs_per_sample{std::chrono::duration_cast(m_range) / DESIRED_SAMPLES}; - timer->stop(); - timer->setInterval(msecs_per_sample); + bool success = loadDataFromBinary(); + + if (!success) { // Zero the values + LogPrintf("TrafficGraphWidget: Saved traffic data was invalid.\n"); + m_baseline_bytes_recv = m_baseline_bytes_sent = 0; + for (int i = 0; i < VALUES_SIZE; i++) { + m_last_bytes_in[i] = m_last_bytes_out[i] = m_last_time[i] = 0; + m_samples_in[i].clear(); + m_samples_out[i].clear(); + m_time_stamp[i].clear(); + } + return false; + } - clear(); + // If we successfully loaded data, determine the correct band to use + int firstNonFullBand = VALUES_SIZE - 1; + + for (int i = 0; i < VALUES_SIZE; i++) { + if (m_time_stamp[i].size() < DESIRED_SAMPLES) { + firstNonFullBand = i; + break; + } + } + + if (firstNonFullBand) { // not the first band + m_value = firstNonFullBand - 1; // Minus one as we're bumping it + m_bump = true; // Set the slider to the new range + } + + return true; } -void TrafficGraphWidget::clear() +int TrafficGraphWidget::findClosestPointByTimestamp(int dst_range) const { - timer->stop(); + if (!m_tt_point || m_tt_point > m_time_stamp[m_value].size() || + m_time_stamp[dst_range].empty()) { + return 0; + } + + int src_point = m_tt_point - 1; + bool is_peak = false, is_dip = false; + float src_value = m_tt_in_series ? m_samples_in[m_value].at(src_point) : + m_samples_out[m_value].at(src_point); + int64_t src_timestamp = m_time_stamp[m_value].at(src_point); + + if (src_point > 0 && src_point < m_time_stamp[m_value].size() - 1) { + float prev_value = m_tt_in_series ? m_samples_in[m_value].at(src_point - 1) : + m_samples_out[m_value].at(src_point - 1); + float next_value = m_tt_in_series ? m_samples_in[m_value].at(src_point + 1) : + m_samples_out[m_value].at(src_point + 1); + + is_peak = src_value > prev_value && src_value > next_value; + is_dip = src_value < prev_value && src_value < next_value; + } + + int dst_point = 0; + uint64_t avg_sample_interval = (m_values[dst_range] * 60 * 1000) / DESIRED_SAMPLES; + int64_t time_window = avg_sample_interval * 3; + int64_t min_difference = time_window * 2; + + // Find the nearest point timestamp-wise + for (int i = 0; i < m_time_stamp[dst_range].size(); ++i) { + auto diff = std::abs(m_time_stamp[dst_range].at(i) - src_timestamp); + if (diff < min_difference) { + min_difference = diff; + dst_point = i; + } + } + + // Exit early if no point found or not a peak nor a dip + if (!dst_point || (!is_peak && !is_dip)) return dst_point; - vSamplesOut.clear(); - vSamplesIn.clear(); - fMax = 0.0f; + // If a peak/dip, snap to a nearby peak/dip if one exists + float dst_value = m_tt_in_series ? m_samples_in[dst_range].at(dst_point - 1) : + m_samples_out[dst_range].at(dst_point - 1); + float best_value = dst_value; + int best_point = dst_point - 1; - if(clientModel) { - nLastBytesIn = clientModel->node().getTotalBytesRecv(); - nLastBytesOut = clientModel->node().getTotalBytesSent(); + for (int i = best_point - 3; i <= best_point + 3; ++i) { + if (i < 0 || i >= m_time_stamp[dst_range].size()) continue; + if (std::abs(m_time_stamp[dst_range].at(i) - src_timestamp) > time_window) continue; + float value = m_tt_in_series ? m_samples_in[dst_range].at(i) : m_samples_out[dst_range].at(i); + if (is_peak && value > best_value) { + dst_point = i + 1; + best_value = value; + } else if (is_dip && value < best_value) { + dst_point = i + 1; + best_value = value; + } } - timer->start(); + + return dst_point; } diff --git a/src/qt/trafficgraphwidget.h b/src/qt/trafficgraphwidget.h index 5e5557ec82a..e6e7382c6b6 100644 --- a/src/qt/trafficgraphwidget.h +++ b/src/qt/trafficgraphwidget.h @@ -5,8 +5,10 @@ #ifndef BITCOIN_QT_TRAFFICGRAPHWIDGET_H #define BITCOIN_QT_TRAFFICGRAPHWIDGET_H -#include +#include +#include #include +#include #include @@ -17,34 +19,68 @@ class QPaintEvent; class QTimer; QT_END_NAMESPACE +static constexpr int VALUES_SIZE = 13; + class TrafficGraphWidget : public QWidget { Q_OBJECT public: - explicit TrafficGraphWidget(QWidget *parent = nullptr); - void setClientModel(ClientModel *model); - std::chrono::minutes getGraphRange() const; + explicit TrafficGraphWidget(QWidget* parent = nullptr); + void setClientModel(ClientModel* model); + bool graphRangeBump() const { return m_bump; } + unsigned int getCurrentRangeIndex() const { return m_new_value; } + quint64 getBaselineBytesRecv() const { return m_baseline_bytes_recv; } + quint64 getBaselineBytesSent() const { return m_baseline_bytes_sent; } protected: - void paintEvent(QPaintEvent *) override; + void paintEvent(QPaintEvent*) override; + int yValue(float) const; + void mouseMoveEvent(QMouseEvent*) override; + void mousePressEvent(QMouseEvent*) override; + void leaveEvent(QEvent*) override; + int findClosestPointByTimestamp(int) const; public Q_SLOTS: - void updateRates(); - void setGraphRange(std::chrono::minutes new_range); - void clear(); + void updateStuff(); + int setGraphRange(int); private: - void paintPath(QPainterPath &path, QQueue &samples); - - QTimer* timer{nullptr}; - float fMax{0.0f}; - std::chrono::minutes m_range{0}; - QQueue vSamplesIn; - QQueue vSamplesOut; - quint64 nLastBytesIn{0}; - quint64 nLastBytesOut{0}; - ClientModel* clientModel{nullptr}; + void saveData(); + int paintPath(QPainterPath&, const QQueue&); + bool loadDataFromBinary(); + bool loadData(); + void updateFmax(); + void updateRates(int, int64_t, quint64, quint64); + void focusSlider(); + void drawTooltipPoint(QPainter&); + + QTimer* m_timer{nullptr}; + float m_fmax{1.1f}; + float m_new_fmax{1.1f}; + float m_range{0}; + QQueue m_samples_in[VALUES_SIZE] = {}; + QQueue m_samples_out[VALUES_SIZE] = {}; + QQueue m_time_stamp[VALUES_SIZE] = {}; + quint64 m_last_bytes_in[VALUES_SIZE] = {}; + quint64 m_last_bytes_out[VALUES_SIZE] = {}; + int64_t m_last_time[VALUES_SIZE] = {}; + ClientModel* m_client_model{nullptr}; + int m_value{0}; + int m_new_value{0}; + bool m_bump{false}; + bool m_toggle{true}; // Default to logarithmic + bool m_update{false}; // whether to redraw graph + int m_tt_point{0}; // 0 = no tooltip (array index + 1) + bool m_tt_in_series{true}; // true = in, false = out + int m_x_offset{0}; + int m_y_offset{0}; + int64_t m_tt_time{0}; + int m_values[VALUES_SIZE] = {5, 10, 20, 45, 90, 3*60, 6*60, 12*60, 24*60, 3*24*60, 7*24*60, 14*24*60, 28*24*60}; + std::string m_data_dir; + interfaces::Node* m_node; + quint64 m_baseline_bytes_recv{0}; + quint64 m_baseline_bytes_sent{0}; }; #endif // BITCOIN_QT_TRAFFICGRAPHWIDGET_H