From 592a78b9a0c42d79a19ac829a8e8e96465bf4d01 Mon Sep 17 00:00:00 2001 From: R E Broadley Date: Wed, 2 Apr 2025 13:21:18 +0100 Subject: [PATCH] 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/rpcconsole.cpp | 61 +++- src/qt/rpcconsole.h | 6 +- src/qt/trafficgraphwidget.cpp | 655 +++++++++++++++++++++++++--------- src/qt/trafficgraphwidget.h | 84 +++-- 5 files changed, 620 insertions(+), 213 deletions(-) diff --git a/src/qt/forms/debugwindow.ui b/src/qt/forms/debugwindow.ui index 6450680c7c70b..fd38192ce0b2e 100644 --- a/src/qt/forms/debugwindow.ui +++ b/src/qt/forms/debugwindow.ui @@ -841,19 +841,28 @@ - 1 + 0 - 288 + 2400 + + + 200 - 12 + 400 - 6 + 0 Qt::Horizontal + + + QSlider::TicksBelow + + + 200 @@ -870,16 +879,6 @@ - - - - &Reset - - - false - - - diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index 696eecaae5ca9..abeb23ab17c50 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -55,7 +55,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"; @@ -576,7 +575,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); @@ -588,7 +586,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(); @@ -1258,21 +1256,64 @@ 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); + static int64_t last_click_time = 0; + static bool last_click_was_up = false; + unsigned int range = (value + 100) / 200 + 1; // minimum of 1, 0 reserve for scale bump + bool bouncing = false; + if (!m_slider_in_use) { + // Avoid accidental oscillation of direction due to rapid mouse clicks + int64_t now = GetTime().count(); + bool this_click_is_up = false; + if (value > m_set_slider_value) this_click_is_up = true; + if (now - last_click_time < 250 && this_click_is_up != last_click_was_up) { + bouncing = true; + ui->sldGraphRange->blockSignals(true); + ui->sldGraphRange->setValue(m_set_slider_value); + ui->sldGraphRange->blockSignals(false); + } + last_click_time = now; + last_click_was_up = this_click_is_up; + } + m_set_slider_value = value; + if (bouncing) return; + setTrafficGraphRange(range); } -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::resetDetailWidget() diff --git a/src/qt/rpcconsole.h b/src/qt/rpcconsole.h index 0cc1a297d2498..3a0b473f22857 100644 --- a/src/qt/rpcconsole.h +++ b/src/qt/rpcconsole.h @@ -94,6 +94,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; @@ -152,7 +154,7 @@ public Q_SLOTS: } const ts; void startExecutor(); - void setTrafficGraphRange(int mins); + void setTrafficGraphRange(int value); void WriteCommandHistory(); enum ColumnWidths @@ -189,6 +191,8 @@ public Q_SLOTS: QByteArray m_peer_widget_header_state; QByteArray m_banlist_widget_header_state; bool m_alternating_row_colors{false}; + 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 8a52baebb3515..a08a58079ef3f 100644 --- a/src/qt/trafficgraphwidget.cpp +++ b/src/qt/trafficgraphwidget.cpp @@ -2,18 +2,15 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include #include #include #include -#include #include #include #include #include #include - #include #include @@ -23,95 +20,153 @@ #define YMARGIN 10 TrafficGraphWidget::TrafficGraphWidget(QWidget* parent) - : QWidget(parent), - vSamplesIn(), - vSamplesOut() + : QWidget(parent) { - timer = new QTimer(this); - tt_timer = new QTimer(this); - connect(timer, &QTimer::timeout, this, &TrafficGraphWidget::updateRates); - connect(tt_timer, &QTimer::timeout, this, &TrafficGraphWidget::updateToolTip); - tt_timer->setInterval(500); - tt_timer->start(); + m_timer = new QTimer(this); + connect(m_timer, &QTimer::timeout, this, &TrafficGraphWidget::updateStuff); + m_timer->setInterval(75); + m_timer->start(); setMouseTracking(true); + setFocusPolicy(Qt::StrongFocus); // Make widget focusable to respond to keyboard events } 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::y_value(float value) +int TrafficGraphWidget::y_value(float value) const { int h = height() - YMARGIN * 2; - return YMARGIN + h - (h * 1.0 * (fToggle ? (pow(value, 0.30102) / pow(fMax, 0.30102)) : (value / fMax))); + return YMARGIN + h - (h * 1.0 * (m_toggle ? (std::pow(value, 0.30102) / std::pow(fMax, 0.30102)) : (value / fMax))); } -void TrafficGraphWidget::paintPath(QPainterPath &path, QQueue &samples) +void 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 = y_value(samples.at(i)); - path.lineTo(x, y); - } - path.lineTo(x, YMARGIN + h); + if (sampleCount <= 0) return; + 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) { + double ratio = static_cast(i) * m_values[m_value] / m_range / DESIRED_SAMPLES; + x = XMARGIN + w - static_cast(w * ratio); + path.lineTo(x, y_value(samples.at(i))); + } + path.lineTo(x, YMARGIN + h); +} + +void TrafficGraphWidget::focusSlider(Qt::FocusReason reason) +{ + QWidget* parent = parentWidget(); + if (parent) { + QSlider* slider = parent->findChild("sldGraphRange"); + if (slider) slider->setFocus(reason); } } -void TrafficGraphWidget::mousePressEvent(QMouseEvent *event) +void TrafficGraphWidget::mousePressEvent(QMouseEvent* event) { + focusSlider(Qt::MouseFocusReason); QWidget::mousePressEvent(event); - fToggle = !fToggle; + m_toggle = !m_toggle; update(); } -float floatmax(float a, float b) +void TrafficGraphWidget::mouseReleaseEvent(QMouseEvent* event) +{ + QWidget::mouseReleaseEvent(event); + focusSlider(Qt::MouseFocusReason); +} + +void TrafficGraphWidget::focusInEvent(QFocusEvent* event) { - if (a > b) return a; - else return b; + QWidget::focusInEvent(event); + // When widget gets focus through any means (like tab navigation) + focusSlider(Qt::OtherFocusReason); } -void TrafficGraphWidget::mouseMoveEvent(QMouseEvent *event) +void TrafficGraphWidget::mouseMoveEvent(QMouseEvent* event) { QWidget::mouseMoveEvent(event); - static int last_x = -1; - static int last_y = -1; - int x = event->x(); - int y = event->y(); - x_offset = event->globalX() - x; - y_offset = event->globalY() - y; + static int last_x = -1, last_y = -1; + int x = event->x(), y = event->y(); + m_x_offset = event->globalX() - x; m_y_offset = event->globalY() - y; if (last_x == x && last_y == y) return; // Do nothing if mouse hasn't moved int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2; - int i = (w + XMARGIN - x) * DESIRED_SAMPLES / w; - unsigned int smallest_distance = 50; int closest_i = -1; - int sampleSize = vTimeStamp.size(); + int i = (w + XMARGIN - x) * DESIRED_SAMPLES / w, closest_i = -1; + int sampleSize = m_time_stamp[m_value].size(); + unsigned int smallest_distance = 50; + bool is_in_series = true; if (sampleSize && i >= -10 && i < sampleSize + 2 && y <= h + YMARGIN + 3) { for (int test_i = std::max(i - 2, 0); test_i < std::min(i + 10, sampleSize); test_i++) { - float val = floatmax(vSamplesIn.at(test_i), vSamplesOut.at(test_i)); - int y_data = y_value(val); - unsigned int distance = abs(y - y_data); - if (distance < smallest_distance) { - smallest_distance = distance; + float in_val = m_samples_in[m_value].at(test_i), out_val = m_samples_out[m_value].at(test_i); + int y_in = y_value(in_val), y_out = y_value(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); + if (min_distance < smallest_distance) { + smallest_distance = min_distance; closest_i = test_i; + is_in_series = (distance_in <= distance_out); } } } - if (ttpoint != closest_i) { - ttpoint = closest_i; + 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; update(); // Calls paintEvent() to draw or delete the highlighted point } - last_x = x; last_y = y; + last_x = x; + last_y = y; +} + +void TrafficGraphWidget::drawTooltipPoint(QPainter& painter) +{ + int w = width() - XMARGIN * 2; + double ratio = static_cast(m_tt_point) * m_values[m_value] / m_range / DESIRED_SAMPLES; + int x = XMARGIN + w - static_cast(w * ratio); + float inSample = m_samples_in[m_value].at(m_tt_point); + float outSample = m_samples_out[m_value].at(m_tt_point); + float selectedSample = m_tt_in_series ? inSample : outSample; + int y = y_value(selectedSample); + painter.setPen(Qt::yellow); + painter.drawEllipse(QPointF(x, y), 3, 3); + QString strTime; + int64_t sampleTime; + if (m_tt_point + 1 < m_time_stamp[m_value].size()) { + sampleTime = m_time_stamp[m_value].at(m_tt_point + 1); + } else { + strTime = "to "; + sampleTime = m_time_stamp[m_value].at(m_tt_point); + } + int age = GetTime() - sampleTime / 1000; + if (age < 60 * 60 * 23) + strTime += QString::fromStdString(FormatISO8601Time(sampleTime / 1000)); + else + strTime += QString::fromStdString(FormatISO8601DateTime(sampleTime / 1000)); + int nDuration = (m_time_stamp[m_value].at(m_tt_point) - sampleTime); + if (nDuration > 0) { + if (nDuration > 9999) + strTime += " +" + GUIUtil::formatDurationStr(std::chrono::seconds{(nDuration + 500) / 1000}); + else + strTime += " +" + GUIUtil::formatPingTime(std::chrono::microseconds{nDuration * 1000}); + } + QString strData = tr("In") + " " + GUIUtil::formatBytesps(m_samples_in[m_value].at(m_tt_point) * 1000) + " " + tr("Out") + " " + GUIUtil::formatBytesps(m_samples_out[m_value].at(m_tt_point) * 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), strTime + "\n. " + strData); + QToolTip::showText(QPoint(x + m_x_offset, y + m_y_offset), strTime + "\n " + strData); + m_tt_time = GetTime(); } void TrafficGraphWidget::paintEvent(QPaintEvent *) @@ -128,158 +183,438 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *) // decide what order of magnitude we are int base = std::floor(std::log10(fMax)); - float val = std::pow(10.0f, base); + float val = std::pow(10.0f, base); // kB/s - const QString units = tr("kB/s"); const float yMarginText = 2.0; + // draw lines + painter.setPen(axisCol); + for(float y = val; y < fMax; y += val) { + int yy = y_value(y); + painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); + } + painter.drawText(XMARGIN, y_value(val) - yMarginText, GUIUtil::formatBytesps(val * 1000)); + // if we drew 10 or 3 fewer lines, break them up at the next lower order of magnitude - if(fMax / val <= (fToggle ? 10.0f : 3.0f)) { - float oldval = val; - val = pow(10.0f, base - 1); + if (fMax / val <= (m_toggle ? 10.0f : 3.0f)) { + val = std::pow(10.0f, base - 1); painter.setPen(axisCol.darker()); - painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units)); - if (fToggle) { - int yy = y_value(val*0.1); - painter.drawText(XMARGIN, yy-yMarginText, QString("%1 %2").arg(val*0.1).arg(units)); - painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); - } + painter.drawText(XMARGIN, y_value(val) - yMarginText, GUIUtil::formatBytesps(val * 1000)); int count = 1; - for(float y = val; y < (!fToggle || fMax / val < 20 ? fMax : oldval); y += val, count++) { - if(count % 10 == 0) - continue; + for (float y = val; y < (!m_toggle || fMax / val < 20 ? fMax : val*10); y += val, count++) { + // don't overwrite lines drawn above + if (count % 10 == 0) continue; int yy = y_value(y); painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); } - val = oldval; - } - // draw lines - painter.setPen(axisCol); - for(float y = val; y < fMax; y += val) { - int yy = y_value(y); - painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); + if (m_toggle) { + int yy = y_value(val * 0.1); + painter.setPen(axisCol.darker().darker()); + painter.drawText(XMARGIN, yy - yMarginText, GUIUtil::formatBytesps(val * 100)); + painter.drawLine(XMARGIN, yy, width() - XMARGIN, yy); + } } - painter.drawText(XMARGIN, y_value(val)-yMarginText, QString("%1 %2").arg(val).arg(units)); painter.setRenderHint(QPainter::Antialiasing); - if(!vSamplesIn.empty()) { + if (!m_samples_in[m_value].empty()) { 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()) { + if (!m_samples_out[m_value].empty()) { QPainterPath p; - paintPath(p, vSamplesOut); + paintPath(p, m_samples_out[m_value]); painter.fillPath(p, QColor(255, 0, 0, 128)); painter.setPen(Qt::red); painter.drawPath(p); } - int sampleCount = vTimeStamp.size(); - if (ttpoint >= 0 && ttpoint < sampleCount) { - painter.setPen(Qt::yellow); - int w = width() - XMARGIN * 2; - int x = XMARGIN + w - w * ttpoint / DESIRED_SAMPLES; - int y = y_value(floatmax(vSamplesIn.at(ttpoint), vSamplesOut.at(ttpoint))); - painter.drawEllipse(QPointF(x, y), 3, 3); - QString strTime; - int64_t sampleTime = vTimeStamp.at(ttpoint); - int age = GetTime() - sampleTime/1000; - if (age < 60*60*23) - strTime = QString::fromStdString(FormatISO8601Time(sampleTime/1000)); - else - strTime = QString::fromStdString(FormatISO8601DateTime(sampleTime/1000)); - int milliseconds_between_samples = 1000; - if (ttpoint > 0) - milliseconds_between_samples = std::min(milliseconds_between_samples, int(vTimeStamp.at(ttpoint-1) - sampleTime)); - if (ttpoint + 1 < sampleCount) - milliseconds_between_samples = std::min(milliseconds_between_samples, int(sampleTime - vTimeStamp.at(ttpoint+1))); - if (milliseconds_between_samples < 1000) - strTime += QString::fromStdString(strprintf(".%03d", (sampleTime%1000))); - QString strData = tr("In") + " " + GUIUtil::formatBytesps(vSamplesIn.at(ttpoint)*1000) + "\n" + tr("Out") + " " + GUIUtil::formatBytesps(vSamplesOut.at(ttpoint)*1000); - // Line below allows ToolTip to move faster than once every 10 seconds. - QToolTip::showText(QPoint(x + x_offset, y + y_offset), strTime + "\n. " + strData); - QToolTip::showText(QPoint(x + x_offset, y + y_offset), strTime + "\n " + strData); - tt_time = GetTime(); - } else + if (m_tt_point >= 0 && m_tt_point < m_time_stamp[m_value].size() && isVisible() && !window()->isMinimized()) + drawTooltipPoint(painter); + else QToolTip::hideText(); } -void TrafficGraphWidget::updateToolTip() +void TrafficGraphWidget::update_fMax() +{ + 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 = tmax; +} + +/** + * Smoothly updates a value with acceleration/deceleration for animation. + * + * @param new_val 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 update_num(float new_val, float& current, float& increment, int length) { - if (!QToolTip::isVisible()) { - if (ttpoint >= 0) { // Remove the yellow circle if the ToolTip has gone due to mouse moving elsewhere. - ttpoint = -1; - update(); + if (new_val <= 0 || current == new_val) return false; + + if (abs(increment) <= abs(0.8 * current) / length) { // allow equal to as current and increment could be zero + if (new_val > current) + increment = 1.0 * (current + 1) / length; // +1s are to get it started even if current is zero + else + increment = -1.0 * (current + 1) / length; + if (abs(increment) > abs(new_val - current)) { // Only check this when creating an increment + increment = 0; // Nothing to do! + current = new_val; + return true; + } + } else { + if (((increment > 0) && (current + increment * 2 > new_val)) || + ((increment < 0) && (current + increment * 2 < new_val))) { + increment = increment / 2; // Keep the momentum going even if new_val is elsewhere. + } else { + if (((increment > 0) && (current + increment * 8 < new_val)) || + ((increment < 0) && (current + increment * 8 > new_val))) + increment = increment * 2; } - } else if (GetTime() >= tt_time + 9) { // ToolTip is about to expire so refresh it. - update(); } + if (abs(increment) < 0.8 * current / length) { + if ((increment >= 0 && new_val > current) || (increment <= 0 && new_val < current)) { + current = new_val; + increment = 0; + } + } else + current += increment; + if (current <= 0.0f) current = 0.0001f; + + return true; } -void TrafficGraphWidget::updateRates() +void TrafficGraphWidget::updateStuff() { - if(!clientModel) return; - - int64_t nTime = TicksSinceEpoch(SystemClock::now()); - static int64_t nLastTime = nTime - timer->interval(); - int nRealInterval = nTime - nLastTime; - quint64 bytesIn = clientModel->node().getTotalBytesRecv(), - bytesOut = clientModel->node().getTotalBytesSent(); - float in_rate_kilobytes_per_sec = static_cast(bytesIn - nLastBytesIn) / nRealInterval; - float out_rate_kilobytes_per_sec = static_cast(bytesOut - nLastBytesOut) / nRealInterval; - vSamplesIn.push_front(in_rate_kilobytes_per_sec); - vSamplesOut.push_front(out_rate_kilobytes_per_sec); - vTimeStamp.push_front(nLastTime); - nLastTime = nTime; - nLastBytesIn = bytesIn; - nLastBytesOut = bytesOut; - - while(vSamplesIn.size() > DESIRED_SAMPLES) { - vSamplesIn.pop_back(); + if (!m_client_model) return; + int64_t expected_gap = m_timer->interval(); + int64_t now = GetTime().count(); + static int64_t last_jump_time = 0; + int64_t time_offset = 0; + + if (!m_time_stamp[0].empty()) { + int64_t last_time = m_time_stamp[0].front(); + int64_t actual_gap = now - last_time; + if (actual_gap >= 1000 + expected_gap && last_time != last_jump_time) { + time_offset = actual_gap - expected_gap; + last_jump_time = last_time; + } } - while(vSamplesOut.size() > DESIRED_SAMPLES) { - vSamplesOut.pop_back(); + + bool fUpdate = false; + + // Check for new sample and update display if a new sample 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 (time_offset) { + m_offset[i] += time_offset; + if (m_offset[i] > now - m_last_time[i]) m_offset[i] = now - m_last_time[i]; + } + if (now > (m_last_time[i] + msecs_per_sample + m_offset[i] - expected_gap / 2)) { + m_offset[i] = 0; + updateRates(i); + if (i == m_value) { + if (m_tt_point >= 0 && m_tt_point < DESIRED_SAMPLES) { + m_tt_point++; // Move the selected point to the left + if (m_tt_point >= DESIRED_SAMPLES) m_tt_point = -1; + } + fUpdate = true; + } + if (i == m_new_value) update_fMax(); + } } - while(vTimeStamp.size() > DESIRED_SAMPLES) { - vTimeStamp.pop_back(); + time_offset = 0; + + // Update display due to transtion between ranges or new fmax + static float y_increment = 0, x_increment = 0; + if (update_num(m_new_fmax, fMax, y_increment, height() - YMARGIN * 2)) fUpdate = true; + int next_m_value = m_value; + if (update_num(m_values[m_new_value], m_range, x_increment, width() - XMARGIN * 2)) { + if (m_values[m_new_value] > m_range && m_values[m_value] < m_range) { + next_m_value = m_value + 1; + } else if (m_value > 0 && m_values[m_new_value] <= m_range && m_values[m_value - 1] > m_range * 0.99) + next_m_value = m_value - 1; + fUpdate = true; + } else if (m_value != m_new_value) { + next_m_value = m_new_value; + fUpdate = true; } - float tmax = 0.0f; - for (const float f : vSamplesIn) { - if(f > tmax) tmax = f; + if (next_m_value != m_value) { + if (m_tt_point >= 0 && m_tt_point < m_time_stamp[m_value].size()) + m_tt_point = findClosestPointByTimestamp(m_value, m_tt_point, next_m_value); + else + m_tt_point = -1; + m_value = next_m_value; } - for (const float f : vSamplesOut) { - if(f > tmax) tmax = f; + + static bool last_m_toggle = m_toggle; + if (!QToolTip::isVisible() || !isVisible() || window()->isMinimized()) { + if (m_tt_point >= 0) { // Remove the yellow circle if the ToolTip has gone due to mouse moving elsewhere. + if (last_m_toggle == m_toggle) + m_tt_point = -1; + else + last_m_toggle = m_toggle; + fUpdate = true; + } + } else if (m_tt_point >= 0 && GetTime() >= m_tt_time + 9) + fUpdate = true; + + if (fUpdate) update(); +} + +void TrafficGraphWidget::updateRates(int i) +{ + int64_t now = GetTime().count(); + int64_t actual_gap = now - m_last_time[i]; + quint64 bytesIn = m_client_model->node().getTotalBytesRecv(), + bytesOut = m_client_model->node().getTotalBytesSent(); + float in_rate_kilobytes_per_msec = static_cast(bytesIn - m_last_bytes_in[i]) / actual_gap; + float out_rate_kilobytes_per_msec = static_cast(bytesOut - 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_time[i] = now; + m_last_bytes_in[i] = bytesIn; + m_last_bytes_out[i] = bytesOut; + static int8_t fFull[VALUES_SIZE] = {}; + if (fFull[i] == 0 && m_time_stamp[i].size() <= DESIRED_SAMPLES) + fFull[i] = -1; + while (m_time_stamp[i].size() > DESIRED_SAMPLES) { + if (m_tt_point < 0 && m_value == i && i < VALUES_SIZE - 1 && fFull[i] < 0) + m_bump_value = true; + fFull[i] = 1; + m_samples_in[i].pop_back(); + m_samples_out[i].pop_back(); + m_time_stamp[i].pop_back(); } - fMax = tmax; - if (ttpoint >=0 && ttpoint < vTimeStamp.size()) ttpoint++; // Move the selected point to the left - update(); } -void TrafficGraphWidget::setGraphRange(std::chrono::minutes new_range) +int TrafficGraphWidget::setGraphRange(int value) { - m_range = new_range; - const auto msecs_per_sample{std::chrono::duration_cast(m_range) / DESIRED_SAMPLES}; - timer->stop(); - timer->setInterval(msecs_per_sample); + // value is the array marker plus 1 (as zero is reserved for bumping up) + if (!value) { // bump + m_bump_value = false; + 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) update_fMax(); - clear(); + return m_values[m_new_value]; } -void TrafficGraphWidget::clear() +void TrafficGraphWidget::saveData() { - timer->stop(); + 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 + quint64 totalBytesRecv = m_baseline_bytes_recv; + quint64 totalBytesSent = m_baseline_bytes_sent; + if (m_node) { + totalBytesRecv += m_node->getTotalBytesRecv(); + totalBytesSent += m_node->getTotalBytesSent(); + } + + fileout << VARINT(totalBytesRecv) << VARINT(totalBytesSent); + + for (unsigned int i = 0; i < VALUES_SIZE; i++) { + // Save the size of these samples + fileout << VARINT(static_cast(m_time_stamp[i].size())); + + 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; + } + + for (int j = 0; j < m_time_stamp[i].size(); j++) + fileout << VARINT(static_cast(m_time_stamp[i].at(j))); - vSamplesOut.clear(); - vSamplesIn.clear(); - vTimeStamp.clear(); - fMax = 0.0f; + fileout << VARINT(static_cast(m_offset[i])); + } - if(clientModel) { - nLastBytesIn = clientModel->node().getTotalBytesRecv(); - nLastBytesOut = clientModel->node().getTotalBytesSent(); + 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); } - timer->start(); +} + +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; + + quint64 totalBytesRecv = 0; + quint64 totalBytesSent = 0; + + filein >> VARINT(totalBytesRecv) >> VARINT(totalBytesSent); + + // Store the loaded values as our baseline + m_baseline_bytes_recv = totalBytesRecv; + m_baseline_bytes_sent = totalBytesSent; + + for (unsigned int i = 0; i < VALUES_SIZE; i++) { + uint32_t samplesSize; + filein >> VARINT(samplesSize); + + 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); + } + + for (unsigned int j = 0; j < samplesSize; j++) { + uint64_t timeMs; + filein >> VARINT(timeMs); + m_time_stamp[i].push_back(static_cast(timeMs)); + } + + uint64_t offset; + filein >> VARINT(offset); + m_offset[i] = static_cast(offset); + } + + filein.fclose(); + + return true; + } catch (const std::exception& e) { + LogPrintf("TrafficGraphWidget: Error loading data: %s\n", e.what()); + return false; + } +} + +bool TrafficGraphWidget::loadData() +{ + bool success = loadDataFromBinary(); + + if (!success) return false; + + // 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_value = true; + } + + return true; +} + +int TrafficGraphWidget::findClosestPointByTimestamp(int sourceRange, int sourcePoint, int targetRange) const +{ + if (sourcePoint < 0 || sourcePoint >= m_time_stamp[sourceRange].size() || + m_time_stamp[targetRange].empty()) { + return -1; + } + + bool is_peak = false, is_dip = false; + float sourceValue = m_tt_in_series ? m_samples_in[sourceRange].at(sourcePoint) : m_samples_out[sourceRange].at(sourcePoint); + + if (sourcePoint > 0 && sourcePoint < m_time_stamp[sourceRange].size() - 1) { + float prevValue = m_tt_in_series ? m_samples_in[sourceRange].at(sourcePoint - 1) : m_samples_out[sourceRange].at(sourcePoint - 1); + float nextValue = m_tt_in_series ? m_samples_in[sourceRange].at(sourcePoint + 1) : m_samples_out[sourceRange].at(sourcePoint + 1); + + is_peak = sourceValue > prevValue && sourceValue > nextValue; + is_dip = sourceValue < prevValue && sourceValue < nextValue; + } + + int64_t sourceTimestamp = m_time_stamp[sourceRange].at(sourcePoint); + int closestPoint = -1; + int64_t minDifference = std::numeric_limits::max(); + + for (int i = 0; i < m_time_stamp[targetRange].size(); ++i) { + auto diff = std::abs(m_time_stamp[targetRange].at(i) - sourceTimestamp); + if (diff < minDifference) { + minDifference = diff; + closestPoint = i; + } + } + + if (closestPoint >= 0 && (is_peak || is_dip)) { + float closestValue = m_tt_in_series ? m_samples_in[targetRange].at(closestPoint) : m_samples_out[targetRange].at(closestPoint); + int bestPoint = closestPoint; + float bestValue = closestValue; + uint64_t avgSampleInterval = (m_values[targetRange] * 60 * 1000) / DESIRED_SAMPLES; + uint64_t timeWindow = avgSampleInterval * 3; + + for (int i = 0; i < m_time_stamp[targetRange].size(); ++i) { + uint64_t timeDiff = static_cast(std::abs(m_time_stamp[targetRange].at(i) - sourceTimestamp)); + if (timeDiff <= timeWindow) { + float currentValue = m_tt_in_series ? m_samples_in[targetRange].at(i) : m_samples_out[targetRange].at(i); + + if (is_peak && currentValue > bestValue) { + bestPoint = i; + bestValue = currentValue; + } else if (is_dip && currentValue < bestValue) { + bestPoint = i; + bestValue = currentValue; + } + } + } + closestPoint = bestPoint; + } + + return closestPoint; } diff --git a/src/qt/trafficgraphwidget.h b/src/qt/trafficgraphwidget.h index ab4eb3f2599dc..445f9381afd41 100644 --- a/src/qt/trafficgraphwidget.h +++ b/src/qt/trafficgraphwidget.h @@ -5,9 +5,11 @@ #ifndef BITCOIN_QT_TRAFFICGRAPHWIDGET_H #define BITCOIN_QT_TRAFFICGRAPHWIDGET_H -#include +#include +#include #include - +#include +#include #include class ClientModel; @@ -17,45 +19,71 @@ 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_value; } + 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; - int y_value(float value); - void mousePressEvent(QMouseEvent *event) override; - bool fToggle = true; - void mouseMoveEvent(QMouseEvent *event) override; - int ttpoint = -1; - int x_offset = 0; - int y_offset = 0; - int64_t tt_time = 0; + void paintEvent(QPaintEvent*) override; + int y_value(float value) const; + void mouseMoveEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void focusInEvent(QFocusEvent* event) override; + int findClosestPoint(int x, int y, int rangeIndex) const; + int findClosestPointByTimestamp(int sourceRange, int sourcePoint, int targetRange) const; public Q_SLOTS: - void updateRates(); - void updateToolTip(); - void setGraphRange(std::chrono::minutes new_range); - void clear(); + void updateStuff(); + int setGraphRange(int value); private: - void paintPath(QPainterPath &path, QQueue &samples); + void saveData(); + void paintPath(QPainterPath& path, const QQueue& samples); + bool loadDataFromBinary(); + bool loadData(); + void update_fMax(); + void updateRates(int value); + void focusSlider(Qt::FocusReason reason); + void drawTooltipPoint(QPainter& painter); - QTimer* timer{nullptr}; - QTimer* tt_timer{nullptr}; + QTimer* m_timer{nullptr}; float fMax{0.0f}; - std::chrono::minutes m_range{0}; - QQueue vSamplesIn; - QQueue vSamplesOut; - QQueue vTimeStamp; - quint64 nLastBytesIn{0}; - quint64 nLastBytesOut{0}; - ClientModel* clientModel{nullptr}; + 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}; + + float m_new_fmax{0}; + int m_value{0}; + int m_new_value{0}; + bool m_bump_value{false}; + bool m_toggle{true}; + int m_tt_point{-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}; + int64_t m_offset[VALUES_SIZE] = {}; + 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