Files
MARTe_IO_Components/Client/NativeUI-Qt/src/stats_panel.cpp
T
2026-05-27 15:46:50 +02:00

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));
}