Retain network traffic data on slider change

See #2523, a request not to drop data when changing the time scale of
the graph.

This change includes several components performed by @michilumin:

 * allows resampling and data retention
 * separates data from visual sample stores on the graph
 * uses averages instead of maximums for samples
 * improves timing accuracy

Co-authored-by: Michi Lumin <michi@luskwood.org>
This commit is contained in:
chromatic 2022-04-24 09:13:21 -07:00
parent a31fa4ff01
commit 530e7c1008
2 changed files with 233 additions and 80 deletions

View File

@ -1,9 +1,11 @@
// Copyright (c) 2011-2015 The Bitcoin Core developers
// Copyright (c) 2022 The Dogecoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "trafficgraphwidget.h"
#include "clientmodel.h"
#include "utiltime.h"
#include <QPainter>
#include <QPainterPath>
@ -12,32 +14,52 @@
#include <cmath>
#define DESIRED_SAMPLES 800
#define NUM_DISPLAYED_SAMPLES 800
#define BASE_SAMPLES_BUF_SIZE 24 * 60 * 60
#define BASE_SAMPLE_PERIOD_MS 1000
#define XMARGIN 10
#define YMARGIN 10
TrafficGraphWidget::TrafficGraphWidget(QWidget *parent) :
QWidget(parent),
timer(0),
fMax(0.0f),
nMins(0),
vSamplesIn(),
vSamplesOut(),
nLastBytesIn(0),
nLastBytesOut(0),
vTimespanSamplesIn(NUM_DISPLAYED_SAMPLES),
vTimespanSamplesOut(NUM_DISPLAYED_SAMPLES),
nTimespanLastBytesIn(0),
nTimespanLastBytesOut(0),
nTimespanLastSampleTimeMs(0),
nTimespanHeadIndex(0),
timespanTimer(0),
vBaseSamplesIn(BASE_SAMPLES_BUF_SIZE),
vBaseSamplesOut(BASE_SAMPLES_BUF_SIZE),
nBaseLastBytesIn(0),
nBaseLastBytesOut(0),
nBaseLastSampleTimeMs(0),
nBaseSamplesHeadIndex(0),
baseTimer(0),
clientModel(0)
{
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), SLOT(updateRates()));
timespanTimer = new QTimer(this);
connect(timespanTimer, SIGNAL(timeout()), SLOT(updateTimespanRates()));
baseTimer = new QTimer(this);
connect(baseTimer, SIGNAL(timeout()), SLOT(updateBaseRates()));
}
void TrafficGraphWidget::setClientModel(ClientModel *model)
{
clientModel = model;
if(model) {
nLastBytesIn = model->getTotalBytesRecv();
nLastBytesOut = model->getTotalBytesSent();
nTimespanLastBytesIn = model->getTotalBytesRecv();
nTimespanLastBytesOut = model->getTotalBytesSent();
nBaseLastBytesIn = nTimespanLastBytesIn;
nBaseLastBytesOut = nTimespanLastBytesOut;
baseTimer->start(BASE_SAMPLE_PERIOD_MS);
}
}
@ -46,19 +68,80 @@ int TrafficGraphWidget::getGraphRangeMins() const
return nMins;
}
void TrafficGraphWidget::paintPath(QPainterPath &path, QQueue<float> &samples)
float TrafficGraphWidget::resampleSamples(const QVector<float> &source, QVector<float> &dest) const
{
int h = height() - YMARGIN * 2, w = width() - XMARGIN * 2;
int sampleCount = samples.size(), x = XMARGIN + w, y;
if(sampleCount > 0) {
path.moveTo(x, YMARGIN + h);
for(int i = 0; i < sampleCount; ++i) {
x = XMARGIN + w - w * i / DESIRED_SAMPLES;
y = YMARGIN + h - (int)(h * samples.at(i) / fMax);
path.lineTo(x, y);
}
path.lineTo(x, YMARGIN + h);
int numSourceSamples = nMins * 60;
float stride = (float)numSourceSamples / dest.size();
float currentStride = stride;
float maxSample = 0.0f;
float lastAverageRate = 0.0f;
int sourceTailIndex = nBaseSamplesHeadIndex - numSourceSamples;
if (sourceTailIndex < 0) {
sourceTailIndex += BASE_SAMPLES_BUF_SIZE;
}
int currentSampleIndex = sourceTailIndex;
for (int destIndex = 0; destIndex < dest.size(); destIndex++) {
int numSamplesToRead = (int)currentStride;
float aggregate = 0.0f;
for (int i = 0; i < numSamplesToRead; ++i) {
float currentSample = source[currentSampleIndex];
aggregate += currentSample;
currentSampleIndex++;
if (currentSampleIndex >= source.size()) {
currentSampleIndex = 0;
}
}
float averageRate = 0.0f;
if (numSamplesToRead == 0) {
// We seem to be trying to upsample. just use the previous sample.
averageRate = lastAverageRate;
}
else {
averageRate = aggregate / numSamplesToRead;
}
maxSample = std::max(maxSample, averageRate);
dest[destIndex] = averageRate;
lastAverageRate = averageRate;
currentStride -= numSamplesToRead;
currentStride += stride;
}
return maxSample;
}
void TrafficGraphWidget::paintPath(QPainterPath &path, const QVector<float> &samples, int samplesHead)
{
int h = height() - YMARGIN * 2;
int w = width() - XMARGIN * 2;
int lastY = 0;
path.moveTo(XMARGIN, YMARGIN + h);
int samplesIndex = samplesHead;
for (int i = 0; i < samples.size(); ++i) {
int x = w * i / NUM_DISPLAYED_SAMPLES;
int y = h - (int)(h * samples[samplesIndex] / fMax);
path.lineTo(XMARGIN + x, YMARGIN + y);
samplesIndex++;
if (samplesIndex >= samples.size()) {
samplesIndex = 0;
}
lastY = y;
}
path.lineTo(XMARGIN + w, YMARGIN + lastY);
path.lineTo(XMARGIN + w, YMARGIN + h);
}
void TrafficGraphWidget::paintEvent(QPaintEvent *)
@ -66,7 +149,9 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *)
QPainter painter(this);
painter.fillRect(rect(), Qt::black);
if(fMax <= 0.0f) return;
if (fMax <= 0.0f) {
return;
}
QColor axisCol(Qt::gray);
int h = height() - YMARGIN * 2;
@ -79,7 +164,7 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *)
const QString units = tr("KB/s");
const float yMarginText = 2.0;
// draw lines
painter.setPen(axisCol);
painter.drawText(XMARGIN, YMARGIN + h - h * val / fMax-yMarginText, QString("%1 %2").arg(val).arg(units));
@ -87,6 +172,7 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *)
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();
@ -103,74 +189,114 @@ void TrafficGraphWidget::paintEvent(QPaintEvent *)
}
}
if(!vSamplesIn.empty()) {
QPainterPath p;
paintPath(p, vSamplesIn);
painter.fillPath(p, QColor(0, 255, 0, 128));
painter.setPen(Qt::green);
painter.drawPath(p);
}
if(!vSamplesOut.empty()) {
QPainterPath p;
paintPath(p, vSamplesOut);
painter.fillPath(p, QColor(255, 0, 0, 128));
painter.setPen(Qt::red);
painter.drawPath(p);
}
QPainterPath samplesPathIn;
paintPath(samplesPathIn, vTimespanSamplesIn, nTimespanHeadIndex);
painter.fillPath(samplesPathIn, QColor(0, 255, 0, 128));
painter.setPen(Qt::green);
painter.drawPath(samplesPathIn);
QPainterPath samplesPathOut;
paintPath(samplesPathOut, vTimespanSamplesOut, nTimespanHeadIndex);
painter.fillPath(samplesPathOut, QColor(255, 0, 0, 128));
painter.setPen(Qt::red);
painter.drawPath(samplesPathOut);
}
void TrafficGraphWidget::updateRates()
void TrafficGraphWidget::updateTimespanRates()
{
if(!clientModel) return;
quint64 bytesIn = clientModel->getTotalBytesRecv(),
bytesOut = clientModel->getTotalBytesSent();
float inRate = (bytesIn - nLastBytesIn) / 1024.0f * 1000 / timer->interval();
float outRate = (bytesOut - nLastBytesOut) / 1024.0f * 1000 / timer->interval();
vSamplesIn.push_front(inRate);
vSamplesOut.push_front(outRate);
nLastBytesIn = bytesIn;
nLastBytesOut = bytesOut;
while(vSamplesIn.size() > DESIRED_SAMPLES) {
vSamplesIn.pop_back();
}
while(vSamplesOut.size() > DESIRED_SAMPLES) {
vSamplesOut.pop_back();
if (!clientModel) {
return;
}
float tmax = 0.0f;
Q_FOREACH(float f, vSamplesIn) {
if(f > tmax) tmax = f;
int64_t currentTimeMs = GetTimeMillis();
int64_t elapsedMs = currentTimeMs - nTimespanLastSampleTimeMs;
if (elapsedMs <= 0) {
return;
}
Q_FOREACH(float f, vSamplesOut) {
if(f > tmax) tmax = f;
quint64 bytesIn = clientModel->getTotalBytesRecv();
quint64 bytesOut = clientModel->getTotalBytesSent();
float inRate = (bytesIn - nTimespanLastBytesIn) / 1000.0f * (1000.0f / elapsedMs);
float outRate = (bytesOut - nTimespanLastBytesOut) / 1000.0f * (1000.0f / elapsedMs);
nTimespanLastBytesIn = bytesIn;
nTimespanLastBytesOut = bytesOut;
nTimespanLastSampleTimeMs = currentTimeMs;
vTimespanSamplesIn[nTimespanHeadIndex] = inRate;
vTimespanSamplesOut[nTimespanHeadIndex] = outRate;
nTimespanHeadIndex++;
if (nTimespanHeadIndex >= NUM_DISPLAYED_SAMPLES) {
nTimespanHeadIndex = 0;
}
fMax = tmax;
fMax = 0.0f;
const int nInSampleSize = std::min(vTimespanSamplesIn.size(), vTimespanSamplesOut.size());
for (int i = 0; i < nInSampleSize; i++)
{
fMax = std::max(std::max(fMax, vTimespanSamplesIn[i]), vTimespanSamplesOut[i]);
}
update();
}
void TrafficGraphWidget::updateBaseRates()
{
if (!clientModel) {
return;
}
int64_t currentTimeMs = GetTimeMillis();
int64_t elapsedMs = currentTimeMs - nBaseLastSampleTimeMs;
if (elapsedMs <= 0) {
return;
}
quint64 bytesIn = clientModel->getTotalBytesRecv();
quint64 bytesOut = clientModel->getTotalBytesSent();
float inRate = (bytesIn - nBaseLastBytesIn) / 1000.0f * 1000.0f / elapsedMs;
float outRate = (bytesOut - nBaseLastBytesOut) / 1000.0f * 1000.0f / elapsedMs;
nBaseLastBytesIn = bytesIn;
nBaseLastBytesOut = bytesOut;
nBaseLastSampleTimeMs = currentTimeMs;
vBaseSamplesIn[nBaseSamplesHeadIndex] = inRate;
vBaseSamplesOut[nBaseSamplesHeadIndex] = outRate;
nBaseSamplesHeadIndex++;
if (nBaseSamplesHeadIndex >= BASE_SAMPLES_BUF_SIZE) {
nBaseSamplesHeadIndex = 0;
}
}
void TrafficGraphWidget::setGraphRangeMins(int mins)
{
nMins = mins;
int msecsPerSample = nMins * 60 * 1000 / DESIRED_SAMPLES;
timer->stop();
timer->setInterval(msecsPerSample);
nTimespanHeadIndex = 0;
clear();
float maxIn = resampleSamples(vBaseSamplesIn, vTimespanSamplesIn);
float maxOut = resampleSamples(vBaseSamplesOut, vTimespanSamplesOut);
fMax = std::max(maxIn, maxOut);
timespanTimer->start((mins * 60 * 1000) / NUM_DISPLAYED_SAMPLES);
}
void TrafficGraphWidget::clear()
{
timer->stop();
vSamplesOut.clear();
vSamplesIn.clear();
vTimespanSamplesIn.fill(0.0f);
vTimespanSamplesOut.fill(0.0f);
vBaseSamplesIn.fill(0.0f);
vBaseSamplesOut.fill(0.0f);
fMax = 0.0f;
if(clientModel) {
nLastBytesIn = clientModel->getTotalBytesRecv();
nLastBytesOut = clientModel->getTotalBytesSent();
if (clientModel) {
nTimespanLastBytesIn = clientModel->getTotalBytesRecv();
nTimespanLastBytesOut = clientModel->getTotalBytesSent();
nBaseLastBytesIn = nTimespanLastBytesIn;
nBaseLastBytesOut = nTimespanLastBytesOut;
}
timer->start();
}

View File

@ -1,4 +1,5 @@
// Copyright (c) 2011-2015 The Bitcoin Core developers
// Copyright (c) 2022 The Dogecoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
@ -6,7 +7,7 @@
#define BITCOIN_QT_TRAFFICGRAPHWIDGET_H
#include <QWidget>
#include <QQueue>
#include <QVector>
class ClientModel;
@ -28,20 +29,46 @@ protected:
void paintEvent(QPaintEvent *);
public Q_SLOTS:
void updateRates();
void updateTimespanRates();
void updateBaseRates();
void setGraphRangeMins(int mins);
void clear();
private:
void paintPath(QPainterPath &path, QQueue<float> &samples);
void paintPath(QPainterPath &path, const QVector<float> &samples, int samplesHead);
float resampleSamples(const QVector<float> &source, QVector<float> &dest) const;
QTimer *timer;
float fMax;
int nMins;
QQueue<float> vSamplesIn;
QQueue<float> vSamplesOut;
quint64 nLastBytesIn;
quint64 nLastBytesOut;
/*
*These QVector<float>s are implemented as circular buffers so that we always have a maximum of
*BASE_SAMPLES_BUF_SIZE (defined in trafficgraphwidget.cpp, default of 24 hours * 60 minutes * 60 seconds,
*or 86400 samples per day at a samplerate of 1sps) to work with. A second circular "display buffer" is
*implemented with a maximum of NUM_DISPLAYED_SAMPLES (defined in trafficgraphwidget.cpp, default of 800)
*which are the 'visual samples' which can represent different amounts of time according to the slider.
*
*The "base samples" for a given timespan are resampled into the "displayed samples" (in this case 800) to
*redraw and show a history of the selected timespan. When these variables are incremented, we check limits
*to see if we need to wrap around and begin overwriting the oldest values in the circular buffers.
*/
QVector<float> vTimespanSamplesIn;
QVector<float> vTimespanSamplesOut;
quint64 nTimespanLastBytesIn;
quint64 nTimespanLastBytesOut;
int64_t nTimespanLastSampleTimeMs;
int nTimespanHeadIndex;
QTimer *timespanTimer;
QVector<float> vBaseSamplesIn;
QVector<float> vBaseSamplesOut;
quint64 nBaseLastBytesIn;
quint64 nBaseLastBytesOut;
int64_t nBaseLastSampleTimeMs;
int nBaseSamplesHeadIndex;
QTimer *baseTimer;
ClientModel *clientModel;
};