First attempt
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <time.h> // clock_gettime, CLOCK_THREAD_CPUTIME_ID
|
||||
|
||||
namespace kpn {
|
||||
|
||||
using clock_t = std::chrono::steady_clock;
|
||||
using duration_t = std::chrono::duration<double, std::milli>; // milliseconds
|
||||
|
||||
// ── Per-channel statistics ────────────────────────────────────────────────────
|
||||
|
||||
struct ChannelStats {
|
||||
std::atomic<uint64_t> pushes{0};
|
||||
std::atomic<uint64_t> drops{0};
|
||||
std::atomic<uint64_t> overflows{0};
|
||||
std::atomic<uint64_t> pops{0};
|
||||
std::atomic<std::size_t> peak_fill{0};
|
||||
|
||||
ChannelStats() = default;
|
||||
ChannelStats(const ChannelStats&) = delete;
|
||||
ChannelStats& operator=(const ChannelStats&) = delete;
|
||||
|
||||
void record_push(std::size_t current_fill) {
|
||||
pushes.fetch_add(1, std::memory_order_relaxed);
|
||||
std::size_t prev = peak_fill.load(std::memory_order_relaxed);
|
||||
while (current_fill > prev &&
|
||||
!peak_fill.compare_exchange_weak(prev, current_fill,
|
||||
std::memory_order_relaxed, std::memory_order_relaxed))
|
||||
;
|
||||
}
|
||||
void record_drop() { drops.fetch_add(1, std::memory_order_relaxed); }
|
||||
void record_overflow() { overflows.fetch_add(1, std::memory_order_relaxed); }
|
||||
void record_pop() { pops.fetch_add(1, std::memory_order_relaxed); }
|
||||
};
|
||||
|
||||
// ── Per-node statistics ───────────────────────────────────────────────────────
|
||||
|
||||
struct NodeStats {
|
||||
std::atomic<uint64_t> frames_processed{0};
|
||||
|
||||
// Wall-clock execution time EMA — warmup mean for first WARMUP_FRAMES,
|
||||
// then EMA alpha=0.1. Stored as integer microseconds for atomic updates.
|
||||
static constexpr int WARMUP_FRAMES = 5;
|
||||
std::atomic<int64_t> ema_exec_us{0};
|
||||
std::atomic<int64_t> max_exec_us{0};
|
||||
std::atomic<int64_t> total_blocked_us{0};
|
||||
|
||||
// Thread CPU time — actual CPU consumed by this node's thread,
|
||||
// measured via CLOCK_THREAD_CPUTIME_ID. Excludes time sleeping or
|
||||
// blocked on mutexes/channels. Sampled once per frame.
|
||||
std::atomic<int64_t> total_cpu_us{0}; // cumulative CPU µs consumed
|
||||
|
||||
NodeStats() = default;
|
||||
NodeStats(const NodeStats&) = delete;
|
||||
NodeStats& operator=(const NodeStats&) = delete;
|
||||
|
||||
// Call at the start of run_loop to capture thread CPU baseline.
|
||||
// Returns the raw timespec for use in record_exec.
|
||||
static struct timespec cpu_now() {
|
||||
struct timespec ts{};
|
||||
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts);
|
||||
return ts;
|
||||
}
|
||||
|
||||
static int64_t timespec_us(const struct timespec& ts) {
|
||||
return static_cast<int64_t>(ts.tv_sec) * 1'000'000
|
||||
+ static_cast<int64_t>(ts.tv_nsec) / 1'000;
|
||||
}
|
||||
|
||||
void record_exec(duration_t exec_time, duration_t blocked_time,
|
||||
const struct timespec& cpu_before, const struct timespec& cpu_after) {
|
||||
frames_processed.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
int64_t us = static_cast<int64_t>(exec_time.count() * 1000.0);
|
||||
|
||||
uint64_t n = frames_processed.load(std::memory_order_relaxed);
|
||||
int64_t prev = ema_exec_us.load(std::memory_order_relaxed);
|
||||
int64_t next = (n <= static_cast<uint64_t>(WARMUP_FRAMES))
|
||||
? prev + (us - prev) / static_cast<int64_t>(n)
|
||||
: prev + (us - prev) / 10;
|
||||
ema_exec_us.store(next, std::memory_order_relaxed);
|
||||
|
||||
int64_t cur_max = max_exec_us.load(std::memory_order_relaxed);
|
||||
if (us > cur_max)
|
||||
max_exec_us.store(us, std::memory_order_relaxed);
|
||||
|
||||
int64_t blocked_us = static_cast<int64_t>(blocked_time.count() * 1000.0);
|
||||
total_blocked_us.fetch_add(blocked_us, std::memory_order_relaxed);
|
||||
|
||||
int64_t cpu_delta = timespec_us(cpu_after) - timespec_us(cpu_before);
|
||||
if (cpu_delta > 0)
|
||||
total_cpu_us.fetch_add(cpu_delta, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Snapshot for reporting (copyable, taken by watchdog) ─────────────────────
|
||||
|
||||
struct ChannelSnapshot {
|
||||
std::string name;
|
||||
std::size_t capacity;
|
||||
std::size_t current_fill;
|
||||
std::size_t peak_fill;
|
||||
uint64_t pushes;
|
||||
uint64_t drops;
|
||||
uint64_t overflows;
|
||||
uint64_t pops;
|
||||
std::size_t item_bytes; // sizeof(T) for the stored type — set by Channel<T>
|
||||
|
||||
double fill_pct() const {
|
||||
return capacity ? 100.0 * current_fill / capacity : 0.0;
|
||||
}
|
||||
double peak_pct() const {
|
||||
return capacity ? 100.0 * peak_fill / capacity : 0.0;
|
||||
}
|
||||
// Bandwidth in MB/s: bytes transferred / elapsed seconds
|
||||
double bandwidth_mbs(double elapsed_s) const {
|
||||
if (elapsed_s <= 0.0 || item_bytes == 0) return 0.0;
|
||||
return static_cast<double>(pushes * item_bytes) / elapsed_s / 1e6;
|
||||
}
|
||||
};
|
||||
|
||||
struct NodeSnapshot {
|
||||
std::string name;
|
||||
uint64_t frames_processed;
|
||||
double ema_exec_ms;
|
||||
double max_exec_ms;
|
||||
double total_blocked_ms;
|
||||
double throughput_fps;
|
||||
double total_cpu_ms; // cumulative CPU time consumed by this node's thread
|
||||
double cpu_util_pct; // exec_ms / (exec_ms + blocked_ms) * 100
|
||||
};
|
||||
|
||||
} // namespace kpn
|
||||
Reference in New Issue
Block a user