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