#pragma once #include #include #include #include #include #include // clock_gettime, CLOCK_THREAD_CPUTIME_ID namespace kpn { using clock_t = std::chrono::steady_clock; using duration_t = std::chrono::duration; // milliseconds // ── Per-channel statistics ──────────────────────────────────────────────────── struct ChannelStats { std::atomic pushes{0}; std::atomic bytes_pushed{0}; std::atomic drops{0}; std::atomic overflows{0}; std::atomic pops{0}; std::atomic peak_fill{0}; ChannelStats() = default; ChannelStats(const ChannelStats&) = delete; ChannelStats& operator=(const ChannelStats&) = delete; void record_push(std::size_t current_fill, std::size_t data_bytes) { pushes.fetch_add(1, std::memory_order_relaxed); bytes_pushed.fetch_add(data_bytes, 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 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 ema_exec_us{0}; std::atomic max_exec_us{0}; std::atomic 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 total_cpu_us{0}; // cumulative CPU µs consumed // Pool scheduling stats — only meaningful for PoolNode / InterruptNode. // exec_start_us: wall-clock µs when fire_once began; 0 when idle. // Used by the watchdog to detect hung nodes (elapsed > max_exec_time). std::atomic queue_wait_us{0}; // cumulative µs spent in pool queue std::atomic exec_start_us{0}; // non-zero while fire_once is running 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(ts.tv_sec) * 1'000'000 + static_cast(ts.tv_nsec) / 1'000; } void record_queue_wait(duration_t wait) { int64_t us = static_cast(wait.count() * 1000.0); if (us > 0) queue_wait_us.fetch_add(us, std::memory_order_relaxed); } 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(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(WARMUP_FRAMES)) ? prev + (us - prev) / static_cast(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(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 bytes_pushed; // actual bytes accumulated via channel_data_size uint64_t drops; uint64_t overflows; uint64_t pops; std::size_t item_bytes; // sizeof(T) — nominal struct size, not necessarily data size 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: actual bytes transferred / elapsed seconds double bandwidth_mbs(double elapsed_s) const { if (elapsed_s <= 0.0) return 0.0; return static_cast(bytes_pushed) / elapsed_s / 1e6; } }; struct NodeSnapshot { std::string name; uint64_t frames_processed; double ema_exec_ms; double max_exec_ms; double total_blocked_ms; // ThreadPerNode: time blocked in channel pop 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 double queue_wait_ms{0}; // PoolNode: cumulative time spent in pool queue }; // ── Pool statistics + snapshot ──────────────────────────────────────────────── struct PoolSnapshot { std::string name; std::size_t thread_count; std::size_t queue_depth; // tasks waiting in the priority queue std::size_t active_count; // tasks currently executing uint64_t tasks_submitted; uint64_t tasks_completed; }; struct IPoolProbe { virtual ~IPoolProbe() = default; virtual PoolSnapshot snapshot(const std::string& name) const = 0; }; // ── Cross-network snapshot (used by DebugHub) ───────────────────────────────── struct NetworkSnapshot { std::string name; std::vector nodes; std::vector channels; double elapsed_s; }; // ── Resource statistics + snapshot ─────────────────────────────────────────── struct ResourceSnapshot { std::string name; uint64_t acquisitions; double avg_wait_ms; uint64_t peak_waiters; uint64_t current_waiters; bool held; }; struct IResourceProbe { virtual ~IResourceProbe() = default; virtual ResourceSnapshot snapshot(const std::string& name) const = 0; }; } // namespace kpn