From c8979a6a49f2b83c0164b28f2c69c07891a73811 Mon Sep 17 00:00:00 2001 From: jagannatharjun Date: Sat, 26 Dec 2020 21:51:23 +0530 Subject: [PATCH 1/2] 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 --- src/gui/properties/speedplotview.cpp | 195 ++++++++++++++------------- src/gui/properties/speedplotview.h | 69 +++++----- src/gui/properties/speedwidget.cpp | 33 ++--- 3 files changed, 151 insertions(+), 146 deletions(-) diff --git a/src/gui/properties/speedplotview.cpp b/src/gui/properties/speedplotview.cpp index 8a3d8f255..817b12ad9 100644 --- a/src/gui/properties/speedplotview.cpp +++ b/src/gui/properties/speedplotview.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Prince Gupta * Copyright (C) 2015 Anton Lashkov * * This program is free software; you can redistribute it and/or @@ -34,33 +35,13 @@ #include #include +#include "base/bittorrent/session.h" #include "base/global.h" #include "base/unicodestrings.h" #include "base/utils/misc.h" 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 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 &sink) - : m_divider(divider) - , m_sink(sink) - , m_counter(0) - , m_accumulator {} +SpeedPlotView::Averager::Averager(const milliseconds duration, const milliseconds resolution) + : m_resolution {resolution} + , m_maxDuration {duration} + , m_sink {static_cast(duration / resolution)} { + 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, // 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. // With quint64 this speed limit is 2^64/144 ~~ 114 PBytes/s. // This speed is inaccessible to an ordinary user. - m_accumulator.x += pointData.x; + ++m_counter; for (int id = UP; id < NB_GRAPHS; ++id) - m_accumulator.y[id] += pointData.y[id]; - m_counter = (m_counter + 1) % m_divider; - if (m_counter != 0) - return; // still accumulating + m_accumulator[id] += sampleData[id]; + + // system may go to sleep, that can cause very big elapsed interval + const milliseconds updateInterval {static_cast(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 for (int id = UP; id < NB_GRAPHS; ++id) - m_accumulator.y[id] /= m_divider; - m_accumulator.x /= m_divider; + m_accumulator[id] /= m_counter; + + 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 - 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_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) - : 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) + : QGraphicsView {parent} { QPen greenPen; greenPen.setWidthF(1.5); @@ -205,69 +200,61 @@ void SpeedPlotView::setGraphEnable(GraphID id, bool enable) viewport()->update(); } -void SpeedPlotView::pushPoint(const SpeedPlotView::PointData &point) +void SpeedPlotView::pushPoint(const SpeedPlotView::SampleData &point) { - m_data5Min.push_back(point); - m_averager30Min.push(point); - m_averager6Hour.push(point); - m_averager12Hour.push(point); - m_averager24Hour.push(point); + for (Averager *averager : {&m_averager5Min, &m_averager30Min + , &m_averager6Hour, &m_averager12Hour + , &m_averager24Hour}) + { + if (averager->push(point)) + { + if (m_currentAverager == averager) + viewport()->update(); + } + } } void SpeedPlotView::setPeriod(const TimePeriod period) { - m_period = period; - switch (period) { case SpeedPlotView::MIN1: - m_viewablePointsCount = MIN1_SEC; - m_currentData = &m_data5Min; + m_currentMaxDuration = 1min; + m_currentAverager = &m_averager5Min; break; case SpeedPlotView::MIN5: - m_viewablePointsCount = MIN5_SEC; - m_currentData = &m_data5Min; + m_currentMaxDuration = 5min; + m_currentAverager = &m_averager5Min; break; case SpeedPlotView::MIN30: - m_viewablePointsCount = MIN30_BUF_SIZE; - m_currentData = &m_data30Min; + m_currentMaxDuration = 30min; + m_currentAverager = &m_averager30Min; break; case SpeedPlotView::HOUR6: - m_viewablePointsCount = HOUR6_BUF_SIZE; - m_currentData = &m_data6Hour; + m_currentMaxDuration = 6h; + m_currentAverager = &m_averager6Hour; break; case SpeedPlotView::HOUR12: - m_viewablePointsCount = HOUR12_BUF_SIZE; - m_currentData = &m_data12Hour; + m_currentMaxDuration = 12h; + m_currentAverager = &m_averager12Hour; break; case SpeedPlotView::HOUR24: - m_viewablePointsCount = HOUR24_BUF_SIZE; - m_currentData = &m_data24Hour; + m_currentMaxDuration = 24h; + m_currentAverager = &m_averager24Hour; break; } viewport()->update(); } -void SpeedPlotView::replot() -{ - if ((m_period == MIN1) - || (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::getCurrentData() +const SpeedPlotView::DataCircularBuffer &SpeedPlotView::currentData() const { - return *m_currentData; + return m_currentAverager->data(); } -quint64 SpeedPlotView::maxYValue() +quint64 SpeedPlotView::maxYValue() const { - boost::circular_buffer &queue = getCurrentData(); + const DataCircularBuffer &queue = currentData(); quint64 maxYValue = 0; for (int id = UP; id < NB_GRAPHS; ++id) @@ -276,9 +263,14 @@ quint64 SpeedPlotView::maxYValue() if (!m_properties[static_cast(id)].enable) continue; - for (int i = static_cast(queue.size()) - 1, j = 0; (i >= 0) && (j < m_viewablePointsCount); --i, ++j) - if (queue[i].y[id] > maxYValue) - maxYValue = queue[i].y[id]; + milliseconds duration {0ms}; + for (int i = static_cast(queue.size()) - 1; i >= 0; --i) + { + maxYValue = std::max(maxYValue, queue[i].data[id]); + duration += queue[i].duration; + if (duration >= m_currentMaxDuration) + break; + } } return maxYValue; @@ -350,12 +342,16 @@ void SpeedPlotView::paintEvent(QPaintEvent *) painter.setRenderHints(QPainter::Antialiasing); // 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(rect.height()) / niceScale.sizeInBytes()); - const double xTickSize = static_cast(rect.width()) / (m_viewablePointsCount - 1); + const DataCircularBuffer &queue = currentData(); - boost::circular_buffer &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(rect.width()) / (m_currentMaxDuration - lastDuration).count(); + const double yMultiplier = (niceScale.arg == 0) ? 0 : (static_cast(rect.height()) / niceScale.sizeInBytes()); for (int id = UP; id < NB_GRAPHS; ++id) { @@ -363,18 +359,23 @@ void SpeedPlotView::paintEvent(QPaintEvent *) continue; QVector points; - for (int i = static_cast(queue.size()) - 1, j = 0; (i >= 0) && (j < m_viewablePointsCount); --i, ++j) - { - - int newX = rect.right() - j * xTickSize; - int newY = rect.bottom() - queue[i].y[id] * yMultiplier; + milliseconds duration {0ms}; + for (int i = static_cast(queue.size()) - 1; i >= 0; --i) + { + const int newX = rect.right() - (duration.count() * xTickSize); + const int newY = rect.bottom() - (queue[i].data[id] * yMultiplier); points.push_back(QPoint(newX, newY)); + + duration += queue[i].duration; + if (duration >= m_currentMaxDuration) + break; } painter.setPen(m_properties[static_cast(id)].pen); painter.drawPolyline(points.data(), points.size()); } + painter.setClipping(false); // draw legend QPoint legendTopLeft(rect.left() + 4, fullRect.top() + 4); diff --git a/src/gui/properties/speedplotview.h b/src/gui/properties/speedplotview.h index b4db1df26..f6ad23997 100644 --- a/src/gui/properties/speedplotview.h +++ b/src/gui/properties/speedplotview.h @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2021 Prince Gupta * Copyright (C) 2015 Anton Lashkov * * This program is free software; you can redistribute it and/or @@ -28,15 +29,22 @@ #pragma once +#include +#include + #ifndef Q_MOC_RUN #include #endif +#include #include #include class QPen; +using std::chrono::milliseconds; +using namespace std::chrono_literals; + class SpeedPlotView final : public QGraphicsView { Q_OBJECT @@ -68,37 +76,44 @@ public: HOUR24 }; - struct PointData - { - qint64 x; - quint64 y[NB_GRAPHS]; - }; + using SampleData = std::array; explicit SpeedPlotView(QWidget *parent = nullptr); void setGraphEnable(GraphID id, bool enable); void setPeriod(TimePeriod period); - void pushPoint(const PointData &point); - - void replot(); + void pushPoint(const SampleData &point); protected: void paintEvent(QPaintEvent *event) override; private: + struct Sample + { + milliseconds duration; + SampleData data; + }; + + using DataCircularBuffer = boost::circular_buffer; + class Averager { public: - Averager(int divider, boost::circular_buffer &sink); - void push(const PointData &pointData); - bool isReady() const; + Averager(milliseconds duration, milliseconds resolution); + + bool push(const SampleData &sampleData); // returns true if there is new data to display + + const DataCircularBuffer &data() const; private: - const int m_divider; - boost::circular_buffer &m_sink; - int m_counter; - PointData m_accumulator; + const milliseconds m_resolution; + const milliseconds m_maxDuration; + milliseconds m_currentDuration {0ms}; + int m_counter = 0; + SampleData m_accumulator {}; + DataCircularBuffer m_sink {}; + QElapsedTimer m_lastSampleTime; }; struct GraphProperties @@ -111,22 +126,16 @@ private: bool enable; }; - quint64 maxYValue(); - boost::circular_buffer &getCurrentData(); + quint64 maxYValue() const; + const DataCircularBuffer ¤tData() const; - boost::circular_buffer m_data5Min; - boost::circular_buffer m_data30Min; - boost::circular_buffer m_data6Hour; - boost::circular_buffer m_data12Hour; - boost::circular_buffer m_data24Hour; - boost::circular_buffer *m_currentData; - Averager m_averager30Min; - Averager m_averager6Hour; - Averager m_averager12Hour; - Averager m_averager24Hour; + Averager m_averager5Min {5min, 1s}; + Averager m_averager30Min {30min, 6s}; + Averager m_averager6Hour {6h, 36s}; + Averager m_averager12Hour {12h, 72s}; + Averager m_averager24Hour {24h, 144s}; + Averager *m_currentAverager {&m_averager5Min}; QMap m_properties; - - TimePeriod m_period; - int m_viewablePointsCount; + milliseconds m_currentMaxDuration; }; diff --git a/src/gui/properties/speedwidget.cpp b/src/gui/properties/speedwidget.cpp index 880d5921e..802069dc1 100644 --- a/src/gui/properties/speedwidget.cpp +++ b/src/gui/properties/speedwidget.cpp @@ -109,16 +109,13 @@ SpeedWidget::SpeedWidget(PropertiesWidget *parent) m_hlayout->addWidget(m_graphsButton); m_plot = new SpeedPlotView(this); + connect(BitTorrent::Session::instance(), &BitTorrent::Session::statsUpdated, this, &SpeedWidget::update); m_layout->addLayout(m_hlayout); m_layout->addWidget(m_plot); loadSettings(); - QTimer *localUpdateTimer = new QTimer(this); - connect(localUpdateTimer, &QTimer::timeout, this, &SpeedWidget::update); - localUpdateTimer->start(1000); - m_plot->show(); } @@ -133,21 +130,19 @@ void SpeedWidget::update() { const BitTorrent::SessionStatus &btStatus = BitTorrent::Session::instance()->status(); - SpeedPlotView::PointData point; - point.x = QDateTime::currentMSecsSinceEpoch() / 1000; - point.y[SpeedPlotView::UP] = btStatus.uploadRate; - point.y[SpeedPlotView::DOWN] = btStatus.downloadRate; - point.y[SpeedPlotView::PAYLOAD_UP] = btStatus.payloadUploadRate; - point.y[SpeedPlotView::PAYLOAD_DOWN] = btStatus.payloadDownloadRate; - point.y[SpeedPlotView::OVERHEAD_UP] = btStatus.ipOverheadUploadRate; - point.y[SpeedPlotView::OVERHEAD_DOWN] = btStatus.ipOverheadDownloadRate; - point.y[SpeedPlotView::DHT_UP] = btStatus.dhtUploadRate; - point.y[SpeedPlotView::DHT_DOWN] = btStatus.dhtDownloadRate; - point.y[SpeedPlotView::TRACKER_UP] = btStatus.trackerUploadRate; - point.y[SpeedPlotView::TRACKER_DOWN] = btStatus.trackerDownloadRate; - - m_plot->pushPoint(point); - m_plot->replot(); + SpeedPlotView::SampleData sampleData; + sampleData[SpeedPlotView::UP] = btStatus.uploadRate; + sampleData[SpeedPlotView::DOWN] = btStatus.downloadRate; + sampleData[SpeedPlotView::PAYLOAD_UP] = btStatus.payloadUploadRate; + sampleData[SpeedPlotView::PAYLOAD_DOWN] = btStatus.payloadDownloadRate; + sampleData[SpeedPlotView::OVERHEAD_UP] = btStatus.ipOverheadUploadRate; + sampleData[SpeedPlotView::OVERHEAD_DOWN] = btStatus.ipOverheadDownloadRate; + sampleData[SpeedPlotView::DHT_UP] = btStatus.dhtUploadRate; + sampleData[SpeedPlotView::DHT_DOWN] = btStatus.dhtDownloadRate; + sampleData[SpeedPlotView::TRACKER_UP] = btStatus.trackerUploadRate; + sampleData[SpeedPlotView::TRACKER_DOWN] = btStatus.trackerDownloadRate; + + m_plot->pushPoint(sampleData); } void SpeedWidget::onPeriodChange(int period) From f3e4338efca1f04d679f8a49440a39b48aeeea3d Mon Sep 17 00:00:00 2001 From: jagannatharjun Date: Wed, 30 Dec 2020 12:08:21 +0530 Subject: [PATCH 2/2] Add a 3-Hour graph --- src/gui/properties/speedplotview.cpp | 4 ++++ src/gui/properties/speedplotview.h | 1 + src/gui/properties/speedwidget.cpp | 1 + 3 files changed, 6 insertions(+) diff --git a/src/gui/properties/speedplotview.cpp b/src/gui/properties/speedplotview.cpp index 817b12ad9..3b9d341ac 100644 --- a/src/gui/properties/speedplotview.cpp +++ b/src/gui/properties/speedplotview.cpp @@ -230,6 +230,10 @@ void SpeedPlotView::setPeriod(const TimePeriod period) m_currentMaxDuration = 30min; m_currentAverager = &m_averager30Min; break; + case SpeedPlotView::HOUR3: + m_currentMaxDuration = 3h; + m_currentAverager = &m_averager6Hour; + break; case SpeedPlotView::HOUR6: m_currentMaxDuration = 6h; m_currentAverager = &m_averager6Hour; diff --git a/src/gui/properties/speedplotview.h b/src/gui/properties/speedplotview.h index f6ad23997..464f904b4 100644 --- a/src/gui/properties/speedplotview.h +++ b/src/gui/properties/speedplotview.h @@ -71,6 +71,7 @@ public: MIN1 = 0, MIN5, MIN30, + HOUR3, HOUR6, HOUR12, HOUR24 diff --git a/src/gui/properties/speedwidget.cpp b/src/gui/properties/speedwidget.cpp index 802069dc1..06aafa786 100644 --- a/src/gui/properties/speedwidget.cpp +++ b/src/gui/properties/speedwidget.cpp @@ -71,6 +71,7 @@ SpeedWidget::SpeedWidget(PropertiesWidget *parent) m_periodCombobox->addItem(tr("1 Minute")); m_periodCombobox->addItem(tr("5 Minutes")); m_periodCombobox->addItem(tr("30 Minutes")); + m_periodCombobox->addItem(tr("3 Hours")); m_periodCombobox->addItem(tr("6 Hours")); m_periodCombobox->addItem(tr("12 Hours")); m_periodCombobox->addItem(tr("24 Hours"));