#include "stats_panel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // ── SparkLine ───────────────────────────────────────────────────────── SparkLine::SparkLine(QWidget* parent) : QWidget(parent) { setMinimumSize(60, 24); setMaximumHeight(24); } void SparkLine::setSamples(const std::deque& 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(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(ratio * h); if (barH < 1) continue; float x = r.left() + i * w; // colour: green→yellow→red based on ratio int red = static_cast(255 * std::min(ratio * 2.0f, 1.0f)); int green = static_cast(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>& sources) { using clock = std::chrono::system_clock; double now = std::chrono::duration( clock::now().time_since_epoch()).count(); int n = static_cast(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 lk(src->signals_mutex); nsig = static_cast(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(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)); }