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