Make SpeedPlotView averager time aware

Previously SpeedPlotView assumed speed is updated per second but the
default value was 1500ms and that can be further changed by the
user, this caused a lot of duplicate data in the calculation of the
graph points. Now Averager averages based on the target duration, resolution
and also takes into account when actually data has arrived.

Also improved resolution of 6-hour graph, previously it was same as 12-hour graph
This commit is contained in:
jagannatharjun 2020-12-26 21:51:23 +05:30
parent d7fb2e6403
commit c8979a6a49
3 changed files with 150 additions and 145 deletions

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2021 Prince Gupta <guptaprince8832@gmail.com>
* Copyright (C) 2015 Anton Lashkov <lenton_91@mail.ru> * Copyright (C) 2015 Anton Lashkov <lenton_91@mail.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -34,33 +35,13 @@
#include <QPainter> #include <QPainter>
#include <QPen> #include <QPen>
#include "base/bittorrent/session.h"
#include "base/global.h" #include "base/global.h"
#include "base/unicodestrings.h" #include "base/unicodestrings.h"
#include "base/utils/misc.h" #include "base/utils/misc.h"
namespace namespace
{ {
enum PeriodInSeconds
{
MIN1_SEC = 60,
MIN5_SEC = 5 * 60,
MIN30_SEC = 30 * 60,
HOUR6_SEC = 6 * 60 * 60,
HOUR12_SEC = 12 * 60 * 60,
HOUR24_SEC = 24 * 60 * 60
};
const int MIN5_BUF_SIZE = 5 * 60;
const int MIN30_BUF_SIZE = 5 * 60;
const int HOUR6_BUF_SIZE = 5 * 60;
const int HOUR12_BUF_SIZE = 10 * 60;
const int HOUR24_BUF_SIZE = 10 * 60;
const int DIVIDER_30MIN = MIN30_SEC / MIN30_BUF_SIZE;
const int DIVIDER_6HOUR = HOUR6_SEC / HOUR6_BUF_SIZE;
const int DIVIDER_12HOUR = HOUR12_SEC / HOUR12_BUF_SIZE;
const int DIVIDER_24HOUR = HOUR24_SEC / HOUR24_BUF_SIZE;
// table of supposed nice steps for grid marks to get nice looking quarters of scale // table of supposed nice steps for grid marks to get nice looking quarters of scale
const double roundingTable[] = {1.2, 1.6, 2, 2.4, 2.8, 3.2, 4, 6, 8}; const double roundingTable[] = {1.2, 1.6, 2, 2.4, 2.8, 3.2, 4, 6, 8};
@ -118,55 +99,69 @@ namespace
} }
} }
SpeedPlotView::Averager::Averager(int divider, boost::circular_buffer<PointData> &sink) SpeedPlotView::Averager::Averager(const milliseconds duration, const milliseconds resolution)
: m_divider(divider) : m_resolution {resolution}
, m_sink(sink) , m_maxDuration {duration}
, m_counter(0) , m_sink {static_cast<DataCircularBuffer::size_type>(duration / resolution)}
, m_accumulator {}
{ {
m_lastSampleTime.start();
} }
void SpeedPlotView::Averager::push(const PointData &pointData) bool SpeedPlotView::Averager::push(const SampleData &sampleData)
{ {
// Accumulator overflow will be hit in worst case on longest used averaging span, // Accumulator overflow will be hit in worst case on longest used averaging span,
// defined by divider value. Maximum divider is DIVIDER_24HOUR = 144 // defined by divider value. Maximum divider is DIVIDER_24HOUR = 144
// Using int32 for accumulator we get overflow when transfer speed reaches 2^31/144 ~~ 14.2 MBytes/s. // Using int32 for accumulator we get overflow when transfer speed reaches 2^31/144 ~~ 14.2 MBytes/s.
// With quint64 this speed limit is 2^64/144 ~~ 114 PBytes/s. // With quint64 this speed limit is 2^64/144 ~~ 114 PBytes/s.
// This speed is inaccessible to an ordinary user. // This speed is inaccessible to an ordinary user.
m_accumulator.x += pointData.x; ++m_counter;
for (int id = UP; id < NB_GRAPHS; ++id) for (int id = UP; id < NB_GRAPHS; ++id)
m_accumulator.y[id] += pointData.y[id]; m_accumulator[id] += sampleData[id];
m_counter = (m_counter + 1) % m_divider;
if (m_counter != 0) // system may go to sleep, that can cause very big elapsed interval
return; // still accumulating const milliseconds updateInterval {static_cast<int64_t>(BitTorrent::Session::instance()->refreshInterval() * 1.25)};
const milliseconds maxElapsed {std::max(updateInterval, m_resolution)};
const milliseconds elapsed {std::min(milliseconds {m_lastSampleTime.elapsed()}, maxElapsed)};
if (elapsed < m_resolution)
return false; // still accumulating
// it is time final averaging calculations // it is time final averaging calculations
for (int id = UP; id < NB_GRAPHS; ++id) for (int id = UP; id < NB_GRAPHS; ++id)
m_accumulator.y[id] /= m_divider; m_accumulator[id] /= m_counter;
m_accumulator.x /= m_divider;
m_currentDuration += elapsed;
// remove extra data from front if we reached max duration
if (m_currentDuration > m_maxDuration)
{
// once we go above the max duration never go below that
// otherwise it will cause empty space in graphs
while (!m_sink.empty()
&& ((m_currentDuration - m_sink.front().duration) > m_maxDuration))
{
m_currentDuration -= m_sink.front().duration;
m_sink.pop_front();
}
}
// now flush out averaged data // now flush out averaged data
m_sink.push_back(m_accumulator); Q_ASSERT(m_sink.size() < m_sink.capacity());
m_sink.push_back({elapsed, m_accumulator});
// reset
m_accumulator = {}; m_accumulator = {};
m_counter = 0;
m_lastSampleTime.restart();
return true;
} }
bool SpeedPlotView::Averager::isReady() const const SpeedPlotView::DataCircularBuffer &SpeedPlotView::Averager::data() const
{ {
return m_counter == 0; return m_sink;
} }
SpeedPlotView::SpeedPlotView(QWidget *parent) SpeedPlotView::SpeedPlotView(QWidget *parent)
: QGraphicsView(parent) : QGraphicsView {parent}
, m_data5Min(MIN5_BUF_SIZE)
, m_data30Min(MIN30_BUF_SIZE)
, m_data6Hour(HOUR6_BUF_SIZE)
, m_data12Hour(HOUR12_BUF_SIZE)
, m_data24Hour(HOUR24_BUF_SIZE)
, m_currentData(&m_data5Min)
, m_averager30Min(DIVIDER_30MIN, m_data30Min)
, m_averager6Hour(DIVIDER_6HOUR, m_data6Hour)
, m_averager12Hour(DIVIDER_12HOUR, m_data12Hour)
, m_averager24Hour(DIVIDER_24HOUR, m_data24Hour)
, m_period(MIN5)
, m_viewablePointsCount(MIN5_SEC)
{ {
QPen greenPen; QPen greenPen;
greenPen.setWidthF(1.5); greenPen.setWidthF(1.5);
@ -205,69 +200,61 @@ void SpeedPlotView::setGraphEnable(GraphID id, bool enable)
viewport()->update(); viewport()->update();
} }
void SpeedPlotView::pushPoint(const SpeedPlotView::PointData &point) void SpeedPlotView::pushPoint(const SpeedPlotView::SampleData &point)
{ {
m_data5Min.push_back(point); for (Averager *averager : {&m_averager5Min, &m_averager30Min
m_averager30Min.push(point); , &m_averager6Hour, &m_averager12Hour
m_averager6Hour.push(point); , &m_averager24Hour})
m_averager12Hour.push(point); {
m_averager24Hour.push(point); if (averager->push(point))
{
if (m_currentAverager == averager)
viewport()->update();
}
}
} }
void SpeedPlotView::setPeriod(const TimePeriod period) void SpeedPlotView::setPeriod(const TimePeriod period)
{ {
m_period = period;
switch (period) switch (period)
{ {
case SpeedPlotView::MIN1: case SpeedPlotView::MIN1:
m_viewablePointsCount = MIN1_SEC; m_currentMaxDuration = 1min;
m_currentData = &m_data5Min; m_currentAverager = &m_averager5Min;
break; break;
case SpeedPlotView::MIN5: case SpeedPlotView::MIN5:
m_viewablePointsCount = MIN5_SEC; m_currentMaxDuration = 5min;
m_currentData = &m_data5Min; m_currentAverager = &m_averager5Min;
break; break;
case SpeedPlotView::MIN30: case SpeedPlotView::MIN30:
m_viewablePointsCount = MIN30_BUF_SIZE; m_currentMaxDuration = 30min;
m_currentData = &m_data30Min; m_currentAverager = &m_averager30Min;
break; break;
case SpeedPlotView::HOUR6: case SpeedPlotView::HOUR6:
m_viewablePointsCount = HOUR6_BUF_SIZE; m_currentMaxDuration = 6h;
m_currentData = &m_data6Hour; m_currentAverager = &m_averager6Hour;
break; break;
case SpeedPlotView::HOUR12: case SpeedPlotView::HOUR12:
m_viewablePointsCount = HOUR12_BUF_SIZE; m_currentMaxDuration = 12h;
m_currentData = &m_data12Hour; m_currentAverager = &m_averager12Hour;
break; break;
case SpeedPlotView::HOUR24: case SpeedPlotView::HOUR24:
m_viewablePointsCount = HOUR24_BUF_SIZE; m_currentMaxDuration = 24h;
m_currentData = &m_data24Hour; m_currentAverager = &m_averager24Hour;
break; break;
} }
viewport()->update(); viewport()->update();
} }
void SpeedPlotView::replot() const SpeedPlotView::DataCircularBuffer &SpeedPlotView::currentData() const
{ {
if ((m_period == MIN1) return m_currentAverager->data();
|| (m_period == MIN5)
|| ((m_period == MIN30) && m_averager30Min.isReady())
|| ((m_period == HOUR6) && m_averager6Hour.isReady())
|| ((m_period == HOUR12) && m_averager12Hour.isReady())
|| ((m_period == HOUR24) && m_averager24Hour.isReady()) )
viewport()->update();
} }
boost::circular_buffer<SpeedPlotView::PointData> &SpeedPlotView::getCurrentData() quint64 SpeedPlotView::maxYValue() const
{ {
return *m_currentData; const DataCircularBuffer &queue = currentData();
}
quint64 SpeedPlotView::maxYValue()
{
boost::circular_buffer<PointData> &queue = getCurrentData();
quint64 maxYValue = 0; quint64 maxYValue = 0;
for (int id = UP; id < NB_GRAPHS; ++id) for (int id = UP; id < NB_GRAPHS; ++id)
@ -276,9 +263,14 @@ quint64 SpeedPlotView::maxYValue()
if (!m_properties[static_cast<GraphID>(id)].enable) if (!m_properties[static_cast<GraphID>(id)].enable)
continue; continue;
for (int i = static_cast<int>(queue.size()) - 1, j = 0; (i >= 0) && (j < m_viewablePointsCount); --i, ++j) milliseconds duration {0ms};
if (queue[i].y[id] > maxYValue) for (int i = static_cast<int>(queue.size()) - 1; i >= 0; --i)
maxYValue = queue[i].y[id]; {
maxYValue = std::max(maxYValue, queue[i].data[id]);
duration += queue[i].duration;
if (duration >= m_currentMaxDuration)
break;
}
} }
return maxYValue; return maxYValue;
@ -350,12 +342,16 @@ void SpeedPlotView::paintEvent(QPaintEvent *)
painter.setRenderHints(QPainter::Antialiasing); painter.setRenderHints(QPainter::Antialiasing);
// draw graphs // draw graphs
rect.adjust(3, 0, 0, 0); // Need, else graphs cross left gridline // averager is duration based, it may go little above the maxDuration
painter.setClipping(true);
painter.setClipRect(rect);
const double yMultiplier = (niceScale.arg == 0.0) ? 0.0 : (static_cast<double>(rect.height()) / niceScale.sizeInBytes()); const DataCircularBuffer &queue = currentData();
const double xTickSize = static_cast<double>(rect.width()) / (m_viewablePointsCount - 1);
boost::circular_buffer<PointData> &queue = getCurrentData(); // last point will be drawn at x=0, so we don't need it in the calculation of xTickSize
const milliseconds lastDuration {queue.empty() ? 0ms : queue.back().duration};
const double xTickSize = static_cast<double>(rect.width()) / (m_currentMaxDuration - lastDuration).count();
const double yMultiplier = (niceScale.arg == 0) ? 0 : (static_cast<double>(rect.height()) / niceScale.sizeInBytes());
for (int id = UP; id < NB_GRAPHS; ++id) for (int id = UP; id < NB_GRAPHS; ++id)
{ {
@ -363,18 +359,23 @@ void SpeedPlotView::paintEvent(QPaintEvent *)
continue; continue;
QVector<QPoint> points; QVector<QPoint> points;
for (int i = static_cast<int>(queue.size()) - 1, j = 0; (i >= 0) && (j < m_viewablePointsCount); --i, ++j) milliseconds duration {0ms};
for (int i = static_cast<int>(queue.size()) - 1; i >= 0; --i)
{ {
const int newX = rect.right() - (duration.count() * xTickSize);
int newX = rect.right() - j * xTickSize; const int newY = rect.bottom() - (queue[i].data[id] * yMultiplier);
int newY = rect.bottom() - queue[i].y[id] * yMultiplier;
points.push_back(QPoint(newX, newY)); points.push_back(QPoint(newX, newY));
duration += queue[i].duration;
if (duration >= m_currentMaxDuration)
break;
} }
painter.setPen(m_properties[static_cast<GraphID>(id)].pen); painter.setPen(m_properties[static_cast<GraphID>(id)].pen);
painter.drawPolyline(points.data(), points.size()); painter.drawPolyline(points.data(), points.size());
} }
painter.setClipping(false);
// draw legend // draw legend
QPoint legendTopLeft(rect.left() + 4, fullRect.top() + 4); QPoint legendTopLeft(rect.left() + 4, fullRect.top() + 4);

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2021 Prince Gupta <guptaprince8832@gmail.com>
* Copyright (C) 2015 Anton Lashkov <lenton_91@mail.ru> * Copyright (C) 2015 Anton Lashkov <lenton_91@mail.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -28,15 +29,22 @@
#pragma once #pragma once
#include <array>
#include <chrono>
#ifndef Q_MOC_RUN #ifndef Q_MOC_RUN
#include <boost/circular_buffer.hpp> #include <boost/circular_buffer.hpp>
#endif #endif
#include <QElapsedTimer>
#include <QGraphicsView> #include <QGraphicsView>
#include <QMap> #include <QMap>
class QPen; class QPen;
using std::chrono::milliseconds;
using namespace std::chrono_literals;
class SpeedPlotView final : public QGraphicsView class SpeedPlotView final : public QGraphicsView
{ {
Q_OBJECT Q_OBJECT
@ -68,37 +76,44 @@ public:
HOUR24 HOUR24
}; };
struct PointData using SampleData = std::array<quint64, NB_GRAPHS>;
{
qint64 x;
quint64 y[NB_GRAPHS];
};
explicit SpeedPlotView(QWidget *parent = nullptr); explicit SpeedPlotView(QWidget *parent = nullptr);
void setGraphEnable(GraphID id, bool enable); void setGraphEnable(GraphID id, bool enable);
void setPeriod(TimePeriod period); void setPeriod(TimePeriod period);
void pushPoint(const PointData &point); void pushPoint(const SampleData &point);
void replot();
protected: protected:
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
private: private:
struct Sample
{
milliseconds duration;
SampleData data;
};
using DataCircularBuffer = boost::circular_buffer<Sample>;
class Averager class Averager
{ {
public: public:
Averager(int divider, boost::circular_buffer<PointData> &sink); Averager(milliseconds duration, milliseconds resolution);
void push(const PointData &pointData);
bool isReady() const; bool push(const SampleData &sampleData); // returns true if there is new data to display
const DataCircularBuffer &data() const;
private: private:
const int m_divider; const milliseconds m_resolution;
boost::circular_buffer<PointData> &m_sink; const milliseconds m_maxDuration;
int m_counter; milliseconds m_currentDuration {0ms};
PointData m_accumulator; int m_counter = 0;
SampleData m_accumulator {};
DataCircularBuffer m_sink {};
QElapsedTimer m_lastSampleTime;
}; };
struct GraphProperties struct GraphProperties
@ -111,22 +126,16 @@ private:
bool enable; bool enable;
}; };
quint64 maxYValue(); quint64 maxYValue() const;
boost::circular_buffer<PointData> &getCurrentData(); const DataCircularBuffer &currentData() const;
boost::circular_buffer<PointData> m_data5Min; Averager m_averager5Min {5min, 1s};
boost::circular_buffer<PointData> m_data30Min; Averager m_averager30Min {30min, 6s};
boost::circular_buffer<PointData> m_data6Hour; Averager m_averager6Hour {6h, 36s};
boost::circular_buffer<PointData> m_data12Hour; Averager m_averager12Hour {12h, 72s};
boost::circular_buffer<PointData> m_data24Hour; Averager m_averager24Hour {24h, 144s};
boost::circular_buffer<PointData> *m_currentData; Averager *m_currentAverager {&m_averager5Min};
Averager m_averager30Min;
Averager m_averager6Hour;
Averager m_averager12Hour;
Averager m_averager24Hour;
QMap<GraphID, GraphProperties> m_properties; QMap<GraphID, GraphProperties> m_properties;
milliseconds m_currentMaxDuration;
TimePeriod m_period;
int m_viewablePointsCount;
}; };

View file

@ -109,16 +109,13 @@ SpeedWidget::SpeedWidget(PropertiesWidget *parent)
m_hlayout->addWidget(m_graphsButton); m_hlayout->addWidget(m_graphsButton);
m_plot = new SpeedPlotView(this); m_plot = new SpeedPlotView(this);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::statsUpdated, this, &SpeedWidget::update);
m_layout->addLayout(m_hlayout); m_layout->addLayout(m_hlayout);
m_layout->addWidget(m_plot); m_layout->addWidget(m_plot);
loadSettings(); loadSettings();
QTimer *localUpdateTimer = new QTimer(this);
connect(localUpdateTimer, &QTimer::timeout, this, &SpeedWidget::update);
localUpdateTimer->start(1000);
m_plot->show(); m_plot->show();
} }
@ -133,21 +130,19 @@ void SpeedWidget::update()
{ {
const BitTorrent::SessionStatus &btStatus = BitTorrent::Session::instance()->status(); const BitTorrent::SessionStatus &btStatus = BitTorrent::Session::instance()->status();
SpeedPlotView::PointData point; SpeedPlotView::SampleData sampleData;
point.x = QDateTime::currentMSecsSinceEpoch() / 1000; sampleData[SpeedPlotView::UP] = btStatus.uploadRate;
point.y[SpeedPlotView::UP] = btStatus.uploadRate; sampleData[SpeedPlotView::DOWN] = btStatus.downloadRate;
point.y[SpeedPlotView::DOWN] = btStatus.downloadRate; sampleData[SpeedPlotView::PAYLOAD_UP] = btStatus.payloadUploadRate;
point.y[SpeedPlotView::PAYLOAD_UP] = btStatus.payloadUploadRate; sampleData[SpeedPlotView::PAYLOAD_DOWN] = btStatus.payloadDownloadRate;
point.y[SpeedPlotView::PAYLOAD_DOWN] = btStatus.payloadDownloadRate; sampleData[SpeedPlotView::OVERHEAD_UP] = btStatus.ipOverheadUploadRate;
point.y[SpeedPlotView::OVERHEAD_UP] = btStatus.ipOverheadUploadRate; sampleData[SpeedPlotView::OVERHEAD_DOWN] = btStatus.ipOverheadDownloadRate;
point.y[SpeedPlotView::OVERHEAD_DOWN] = btStatus.ipOverheadDownloadRate; sampleData[SpeedPlotView::DHT_UP] = btStatus.dhtUploadRate;
point.y[SpeedPlotView::DHT_UP] = btStatus.dhtUploadRate; sampleData[SpeedPlotView::DHT_DOWN] = btStatus.dhtDownloadRate;
point.y[SpeedPlotView::DHT_DOWN] = btStatus.dhtDownloadRate; sampleData[SpeedPlotView::TRACKER_UP] = btStatus.trackerUploadRate;
point.y[SpeedPlotView::TRACKER_UP] = btStatus.trackerUploadRate; sampleData[SpeedPlotView::TRACKER_DOWN] = btStatus.trackerDownloadRate;
point.y[SpeedPlotView::TRACKER_DOWN] = btStatus.trackerDownloadRate;
m_plot->pushPoint(point); m_plot->pushPoint(sampleData);
m_plot->replot();
} }
void SpeedWidget::onPeriodChange(int period) void SpeedWidget::onPeriodChange(int period)