Skip to content

Enhanced Traffic Graph Widget with Multi-timeframe Support and Data Persistence #866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions src/qt/forms/debugwindow.ui
Original file line number Diff line number Diff line change
@@ -665,20 +665,29 @@
<item>
<widget class="QSlider" name="sldGraphRange">
<property name="minimum">
<number>1</number>
<number>0</number>
</property>
<property name="maximum">
<number>288</number>
<number>2400</number>
</property>
<property name="singleStep">
<number>200</number>
</property>
<property name="pageStep">
<number>12</number>
<number>200</number>
</property>
<property name="value">
<number>6</number>
<number>0</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>200</number>
</property>
</widget>
</item>
<item>
@@ -694,16 +703,6 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnClearTrafficGraph">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're deleting this, maybe there should be a way for the user to insert a reference line?

For now, I'd move removal of anything to a separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user can effectively remove the data by deleting the .dat file - is there any basis for being able to do this while the client is running?

<property name="text">
<string>&amp;Reset</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
34 changes: 34 additions & 0 deletions src/qt/guiutil.cpp
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions src/qt/guiutil.h
Original file line number Diff line number Diff line change
@@ -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);

41 changes: 31 additions & 10 deletions src/qt/rpcconsole.cpp
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we losing this default constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it isn't needed - on startup it "defaults" to the first non-full range

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()
9 changes: 6 additions & 3 deletions src/qt/rpcconsole.h
Original file line number Diff line number Diff line change
@@ -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();
658 changes: 569 additions & 89 deletions src/qt/trafficgraphwidget.cpp

Large diffs are not rendered by default.

72 changes: 54 additions & 18 deletions src/qt/trafficgraphwidget.h
Original file line number Diff line number Diff line change
@@ -5,8 +5,10 @@
#ifndef BITCOIN_QT_TRAFFICGRAPHWIDGET_H
#define BITCOIN_QT_TRAFFICGRAPHWIDGET_H

#include <QWidget>
#include <QFile>
#include <QKeyEvent>
#include <QQueue>
#include <QWidget>

#include <chrono>

@@ -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<float> &samples);

QTimer* timer{nullptr};
float fMax{0.0f};
std::chrono::minutes m_range{0};
QQueue<float> vSamplesIn;
QQueue<float> vSamplesOut;
quint64 nLastBytesIn{0};
quint64 nLastBytesOut{0};
ClientModel* clientModel{nullptr};
void saveData();
int paintPath(QPainterPath&, const QQueue<float>&);
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<float> m_samples_in[VALUES_SIZE] = {};
QQueue<float> m_samples_out[VALUES_SIZE] = {};
QQueue<int64_t> 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
8 changes: 8 additions & 0 deletions src/util/time.cpp
Original file line number Diff line number Diff line change
@@ -75,6 +75,14 @@ void MockableSteadyClock::ClearMockTime()

int64_t GetTime() { return GetTime<std::chrono::seconds>().count(); }

std::string FormatISO8601Time(int64_t nTime)
{
const std::chrono::sys_seconds secs{std::chrono::seconds{nTime}};
const auto days{std::chrono::floor<std::chrono::days>(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}};
1 change: 1 addition & 0 deletions src/util/time.h
Original file line number Diff line number Diff line change
@@ -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<int64_t> ParseISO8601DateTime(std::string_view str);