269 lines
9.5 KiB
C++
269 lines
9.5 KiB
C++
#include "stats_panel.h"
|
|
|
|
#include <QColor>
|
|
#include <QHBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QLabel>
|
|
#include <QPainter>
|
|
#include <QPushButton>
|
|
#include <QTableWidget>
|
|
#include <QTableWidgetItem>
|
|
#include <QVBoxLayout>
|
|
#include <QWidget>
|
|
#include <chrono>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
// ── SparkLine ─────────────────────────────────────────────────────────
|
|
|
|
SparkLine::SparkLine(QWidget* parent) : QWidget(parent)
|
|
{
|
|
setMinimumSize(60, 24);
|
|
setMaximumHeight(24);
|
|
}
|
|
|
|
void SparkLine::setSamples(const std::deque<float>& hist, float peak)
|
|
{
|
|
samples_.assign(hist.begin(), hist.end());
|
|
peak_ = (peak > 0.0f) ? peak : 1.0f;
|
|
update();
|
|
}
|
|
|
|
void SparkLine::paintEvent(QPaintEvent*)
|
|
{
|
|
QPainter p(this);
|
|
p.setRenderHint(QPainter::Antialiasing);
|
|
|
|
QRect r = rect().adjusted(1, 1, -1, -1);
|
|
p.fillRect(rect(), QColor(20, 20, 20));
|
|
|
|
if (samples_.empty()) return;
|
|
|
|
const int n = static_cast<int>(samples_.size());
|
|
const float w = float(r.width()) / n;
|
|
const int h = r.height();
|
|
|
|
for (int i = 0; i < n; ++i) {
|
|
float ratio = std::min(samples_[i] / peak_, 1.0f);
|
|
int barH = static_cast<int>(ratio * h);
|
|
if (barH < 1) continue;
|
|
float x = r.left() + i * w;
|
|
// colour: green→yellow→red based on ratio
|
|
int red = static_cast<int>(255 * std::min(ratio * 2.0f, 1.0f));
|
|
int green = static_cast<int>(255 * std::min((1.0f - ratio) * 2.0f, 1.0f));
|
|
p.fillRect(QRectF(x, r.bottom() - barH, w - 1.0f, barH),
|
|
QColor(red, green, 40));
|
|
}
|
|
}
|
|
|
|
// ── helpers ───────────────────────────────────────────────────────────
|
|
|
|
static QString fmtBytes(uint64_t b)
|
|
{
|
|
if (b < 1024) return QString("%1 B").arg(b);
|
|
if (b < 1024*1024) return QString("%1 KB").arg(b / 1024.0, 0, 'f', 1);
|
|
return QString("%1 MB").arg(b / 1048576.0, 0, 'f', 1);
|
|
}
|
|
|
|
static QString fmtRate(double r)
|
|
{
|
|
if (r < 1e3) return QString("%1/s").arg(r, 0, 'f', 0);
|
|
if (r < 1e6) return QString("%1 k/s").arg(r / 1e3, 0, 'f', 1);
|
|
return QString("%1 M/s").arg(r / 1e6, 0, 'f', 2);
|
|
}
|
|
|
|
// ── StatsPanel ────────────────────────────────────────────────────────
|
|
|
|
static const char* kColHdrs[] = {
|
|
"Source", "Status", "Pkts/s", "Bytes/s",
|
|
"Latency", "Lost %", "Total Pkts", "Signals", "Rate"
|
|
};
|
|
static constexpr int kNCols = 9;
|
|
static constexpr int kRateCol = 8; // SparkLine column
|
|
|
|
StatsPanel::StatsPanel(QWidget* parent) : QWidget(parent)
|
|
{
|
|
setMaximumHeight(28); // collapsed: header only
|
|
|
|
auto* outer = new QVBoxLayout(this);
|
|
outer->setSpacing(0);
|
|
outer->setContentsMargins(0, 0, 0, 0);
|
|
|
|
// ── Header bar (always visible) ───────────────────────────────────
|
|
auto* hdr = new QWidget(this);
|
|
hdr->setFixedHeight(28);
|
|
hdr->setObjectName("statsPanelHeader");
|
|
auto* hdrLay = new QHBoxLayout(hdr);
|
|
hdrLay->setContentsMargins(6, 0, 6, 0);
|
|
hdrLay->setSpacing(8);
|
|
|
|
toggleBtn_ = new QPushButton("\u25b2 Stats", hdr);
|
|
toggleBtn_->setFlat(true);
|
|
toggleBtn_->setFixedWidth(80);
|
|
toggleBtn_->setObjectName("statsPanelToggle");
|
|
connect(toggleBtn_, &QPushButton::clicked, this, &StatsPanel::toggle);
|
|
hdrLay->addWidget(toggleBtn_);
|
|
|
|
summaryLbl_ = new QLabel("No sources connected", hdr);
|
|
summaryLbl_->setStyleSheet("color:#888; font-size:11px;");
|
|
hdrLay->addWidget(summaryLbl_, 1);
|
|
outer->addWidget(hdr);
|
|
|
|
// ── Expandable content ────────────────────────────────────────────
|
|
content_ = new QWidget(this);
|
|
content_->setVisible(false);
|
|
auto* cLay = new QVBoxLayout(content_);
|
|
cLay->setContentsMargins(2, 2, 2, 2);
|
|
cLay->setSpacing(0);
|
|
|
|
table_ = new QTableWidget(0, kNCols, content_);
|
|
QStringList hdrs;
|
|
for (const char* h : kColHdrs) hdrs << QString(h);
|
|
table_->setHorizontalHeaderLabels(hdrs);
|
|
table_->horizontalHeader()->setStretchLastSection(false);
|
|
table_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
|
table_->verticalHeader()->setVisible(false);
|
|
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
table_->setSelectionMode(QAbstractItemView::NoSelection);
|
|
table_->setAlternatingRowColors(true);
|
|
table_->setFixedHeight(130);
|
|
cLay->addWidget(table_);
|
|
|
|
outer->addWidget(content_);
|
|
}
|
|
|
|
void StatsPanel::setExpanded(bool on)
|
|
{
|
|
expanded_ = on;
|
|
content_->setVisible(on);
|
|
toggleBtn_->setText(on ? "\u25bc Stats" : "\u25b2 Stats");
|
|
setMaximumHeight(on ? 200 : 28);
|
|
setMinimumHeight(28);
|
|
}
|
|
|
|
void StatsPanel::buildTable(int nSrc)
|
|
{
|
|
table_->setRowCount(nSrc);
|
|
// Set SparkLine widget in rate column for each row
|
|
for (int r = 0; r < nSrc; ++r) {
|
|
if (!table_->cellWidget(r, kRateCol)) {
|
|
auto* sl = new SparkLine(table_);
|
|
table_->setCellWidget(r, kRateCol, sl);
|
|
table_->setRowHeight(r, 28);
|
|
}
|
|
}
|
|
lastSrcCount_ = nSrc;
|
|
}
|
|
|
|
static QTableWidgetItem* makeCell(const QString& txt,
|
|
Qt::Alignment align = Qt::AlignVCenter | Qt::AlignRight)
|
|
{
|
|
auto* it = new QTableWidgetItem(txt);
|
|
it->setTextAlignment(align);
|
|
return it;
|
|
}
|
|
|
|
void StatsPanel::refresh(const std::vector<std::shared_ptr<Source>>& sources)
|
|
{
|
|
using clock = std::chrono::system_clock;
|
|
double now = std::chrono::duration<double>(
|
|
clock::now().time_since_epoch()).count();
|
|
|
|
int n = static_cast<int>(sources.size());
|
|
if (n != lastSrcCount_) {
|
|
srcStats_.resize(n);
|
|
buildTable(n);
|
|
}
|
|
|
|
uint64_t totalPkts = 0;
|
|
size_t connected = 0;
|
|
double totalPktRate = 0;
|
|
double maxLatency = 0;
|
|
|
|
for (int i = 0; i < n; ++i) {
|
|
auto& src = sources[i];
|
|
auto& ss = srcStats_[i];
|
|
bool conn = src->connected();
|
|
auto st = src->stats();
|
|
if (conn) ++connected;
|
|
|
|
// Rate computation via exponentially-weighted average
|
|
double dt = now - ss.lastTime;
|
|
if (ss.lastTime > 0 && dt > 0.0) {
|
|
double alpha = std::min(dt / 0.5, 1.0); // ~0.5s time constant
|
|
double rawPktRate = double(st.packets_rx - ss.lastPkts) / dt;
|
|
double rawByteRate = rawPktRate * (st.bytes_rx > 0 ?
|
|
double(st.bytes_rx) / double(st.packets_rx > 0 ? st.packets_rx : 1) : 0);
|
|
ss.pktRate = ss.pktRate * (1.0 - alpha) + rawPktRate * alpha;
|
|
ss.byteRate = ss.byteRate * (1.0 - alpha) + rawByteRate * alpha;
|
|
}
|
|
ss.lastPkts = st.packets_rx;
|
|
ss.lastTime = now;
|
|
|
|
// Sparkline history (keep 60 samples)
|
|
ss.rateHist.push_back(float(ss.pktRate));
|
|
if (ss.rateHist.size() > 60) ss.rateHist.pop_front();
|
|
|
|
// Latency: estimate as time since latest buffer write
|
|
double latMs = 0;
|
|
int nsig = 0;
|
|
{
|
|
std::lock_guard<std::mutex> lk(src->signals_mutex);
|
|
nsig = static_cast<int>(src->signal_states.size());
|
|
for (auto& sig : src->signal_states) {
|
|
if (sig.buffer) {
|
|
double lt = sig.buffer->latest_time();
|
|
if (lt > 0) {
|
|
double l = (now - lt) * 1000.0;
|
|
if (l > latMs) latMs = l;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (latMs > maxLatency) maxLatency = latMs;
|
|
|
|
totalPkts += st.packets_rx;
|
|
totalPktRate += ss.pktRate;
|
|
|
|
if (!expanded_) continue;
|
|
|
|
// Loss percentage
|
|
uint64_t total = st.packets_rx + st.packets_lost;
|
|
double lossPct = total > 0 ? 100.0 * st.packets_lost / total : 0.0;
|
|
|
|
QString srcLabel = QString("%1@%2:%3")
|
|
.arg(QString::fromStdString(src->label()))
|
|
.arg(QString::fromStdString(src->host()))
|
|
.arg(src->port());
|
|
|
|
table_->setItem(i, 0, makeCell(srcLabel, Qt::AlignVCenter | Qt::AlignLeft));
|
|
|
|
auto* stIt = makeCell(conn ? "\u25cf Connected" : "\u25cb Disconnected");
|
|
stIt->setForeground(conn ? QColor(80, 200, 80) : QColor(200, 80, 80));
|
|
table_->setItem(i, 1, stIt);
|
|
|
|
table_->setItem(i, 2, makeCell(fmtRate(ss.pktRate)));
|
|
table_->setItem(i, 3, makeCell(fmtBytes(uint64_t(ss.byteRate))));
|
|
|
|
QString latStr = (latMs < 1.0)
|
|
? QString("%1 µs").arg(latMs * 1000.0, 0, 'f', 0)
|
|
: QString("%1 ms").arg(latMs, 0, 'f', 1);
|
|
table_->setItem(i, 4, makeCell(latStr));
|
|
table_->setItem(i, 5, makeCell(QString("%1%").arg(lossPct, 0, 'f', 1)));
|
|
table_->setItem(i, 6, makeCell(QString::number(st.packets_rx)));
|
|
table_->setItem(i, 7, makeCell(QString::number(nsig)));
|
|
|
|
// Update sparkline
|
|
float peak = *std::max_element(ss.rateHist.begin(), ss.rateHist.end());
|
|
if (auto* sl = qobject_cast<SparkLine*>(table_->cellWidget(i, kRateCol)))
|
|
sl->setSamples(ss.rateHist, peak);
|
|
}
|
|
|
|
// Update summary label (always visible)
|
|
summaryLbl_->setText(
|
|
QString("%1/%2 connected | %3 pkts/s | max latency: %4 ms")
|
|
.arg(connected).arg(n)
|
|
.arg(totalPktRate, 0, 'f', 0)
|
|
.arg(maxLatency, 0, 'f', 1));
|
|
}
|