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