/** * @file StatsDialog.cpp */ #include "StatsDialog.h" #include "Hub.h" #include "Theme.h" #include #include #include #include #include #include #include #include #include namespace shq { using StreamHubClient::SourceStats; /* ── HistogramWidget ─────────────────────────────────────────────────────── */ HistogramWidget::HistogramWidget(QWidget* parent) : QWidget(parent) { setMinimumHeight(120); } void HistogramWidget::setData(const SourceStats& st) { min_ = st.cycleHistMin; max_ = st.cycleHistMax; valid_ = (max_ > min_); for (int i = 0; i < 20; i++) { bins_[i] = st.cycleHist[i]; } update(); } void HistogramWidget::paintEvent(QPaintEvent*) { QPainter p(this); p.fillRect(rect(), col::mantle()); if (!valid_) { return; } double maxCount = 0.0; for (int i = 0; i < 20; i++) { if (bins_[i] > maxCount) { maxCount = bins_[i]; } } if (maxCount <= 0.0) { return; } const int L = 4, R = 4, T = 4, B = 16; QRectF area(L, T, width() - L - R, height() - T - B); double bw = area.width() / 20.0; p.setPen(Qt::NoPen); p.setBrush(col::blue()); for (int i = 0; i < 20; i++) { double h = (bins_[i] / maxCount) * area.height(); QRectF bar(area.left() + i * bw + 1, area.bottom() - h, bw - 2, h); p.drawRect(bar); } p.setPen(col::subtext()); QFont f = p.font(); f.setPointSize(7); p.setFont(f); p.drawText(QRectF(L, height() - B, area.width() / 2, B), Qt::AlignLeft | Qt::AlignVCenter, QString::number(min_, 'g', 3) + " ms"); p.drawText(QRectF(L + area.width() / 2, height() - B, area.width() / 2, B), Qt::AlignRight | Qt::AlignVCenter, QString::number(max_, 'g', 3) + " ms"); } /* ── StatsDialog ─────────────────────────────────────────────────────────── */ static const char* kCols[] = { "Source", "State", "Cycles Rx", "Lost", "Rate Hz", "Frags/cyc", "Bytes/cyc", "Cycle ms (avg±std)", "Cycle ms (min/max)" }; static const int kNumCols = 9; StatsDialog::StatsDialog(Hub* hub, QWidget* parent) : QDialog(parent), hub_(hub) { setWindowTitle("Statistics"); resize(760, 460); auto* lay = new QVBoxLayout(this); table_ = new QTableWidget(this); table_->setColumnCount(kNumCols); QStringList headers; for (int i = 0; i < kNumCols; i++) { headers << kCols[i]; } table_->setHorizontalHeaderLabels(headers); table_->verticalHeader()->setVisible(false); table_->setEditTriggers(QAbstractItemView::NoEditTriggers); table_->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); lay->addWidget(table_); auto* scroll = new QScrollArea(this); scroll->setWidgetResizable(true); auto* histHost = new QWidget(scroll); histLay_ = new QVBoxLayout(histHost); scroll->setWidget(histHost); lay->addWidget(scroll, 1); auto* btnRow = new QHBoxLayout(); btnRow->addStretch(1); auto* refreshBtn = new QPushButton("Refresh", this); connect(refreshBtn, &QPushButton::clicked, this, [this]() { hub_->sendGetStats(); refresh(); }); btnRow->addWidget(refreshBtn); lay->addLayout(btnRow); refresh(); } void StatsDialog::refresh() { const auto& sources = hub_->sources(); table_->setRowCount(static_cast(sources.size())); char buf[64]; for (int r = 0; r < static_cast(sources.size()); r++) { const Source& src = sources[r]; const SourceStats& st = src.stats; auto set = [&](int c, const QString& s, const QColor* fg = nullptr) { auto* it = new QTableWidgetItem(s); if (fg) { it->setForeground(*fg); } table_->setItem(r, c, it); }; set(0, QString::fromStdString(src.id)); QColor stc = (st.state == "connected") ? col::green() : col::red(); set(1, QString::fromStdString(st.state), &stc); set(2, QString::number(static_cast(st.totalReceived))); set(3, QString::number(static_cast(st.totalLost))); snprintf(buf, sizeof(buf), "%.2f \u00b1 %.2f", st.rateHz, st.rateStdHz); set(4, buf); snprintf(buf, sizeof(buf), "%.1f", st.fragsPerCycle); set(5, buf); snprintf(buf, sizeof(buf), "%.0f", st.bytesPerCycle); set(6, buf); snprintf(buf, sizeof(buf), "%.3f \u00b1 %.3f", st.cycleAvgMs, st.cycleStdMs); set(7, buf); snprintf(buf, sizeof(buf), "%.3f / %.3f", st.cycleMinMs, st.cycleMaxMs); set(8, buf); } /* Rebuild histogram blocks. */ for (auto* w : histBlocks_) { w->deleteLater(); } histBlocks_.clear(); hists_.clear(); for (const auto& src : sources) { if (src.stats.cycleHistMax <= src.stats.cycleHistMin) { continue; } auto* block = new QWidget(); auto* bl = new QVBoxLayout(block); bl->setContentsMargins(0, 0, 0, 0); bl->addWidget(new QLabel("Cycle time — " + QString::fromStdString(src.id), block)); auto* h = new HistogramWidget(block); h->setData(src.stats); bl->addWidget(h); histLay_->addWidget(block); histBlocks_.push_back(block); hists_.push_back(h); } } } /* namespace shq */