#pragma once #include "diagnostics.hpp" #include "node.hpp" #include "port.hpp" #ifdef KPN_WEB_DEBUG #include "web_debug.hpp" #include #endif #include #include #include #include #include #include #include #include #include #include #include namespace kpn { // ── Exceptions ──────────────────────────────────────────────────────────────── class NetworkCycleError : public std::runtime_error { public: NetworkCycleError() : std::runtime_error("network graph contains a directed cycle") {} }; class NetworkBuildError : public std::runtime_error { public: explicit NetworkBuildError(std::string msg) : std::runtime_error(std::move(msg)) {} }; // ── Network ─────────────────────────────────────────────────────────────────── class Network : public INode { public: using ErrorHandler = std::function; using DiagnosticsHandler = std::function&, const std::vector&)>; // ── Builder API ─────────────────────────────────────────────────────────── template Network& add(std::string name, NodeT& node) { if (nodes_.count(name)) throw NetworkBuildError("duplicate node name: " + name); node.set_name(name); nodes_.emplace(name, &node); adj_[name]; return *this; } template Network& connect(const std::string& src_name, OutputPort, const std::string& dst_name, InputPort) { using out_t = std::tuple_element_t; using dst_in_t = std::tuple_element_t; static_assert(std::is_same_v, "connect: output type does not match input type"); auto* src = dynamic_cast(nodes_.at(src_name)); auto* dst = dynamic_cast(nodes_.at(dst_name)); if (!src) throw NetworkBuildError("node '" + src_name + "' type mismatch"); if (!dst) throw NetworkBuildError("node '" + dst_name + "' type mismatch"); auto port_key = std::make_pair(src_name, SrcIdx); if (connected_outputs_.count(port_key)) throw NetworkBuildError( "connect: output port '" + src_name + "':" + std::to_string(SrcIdx) + " is already connected — use make_fanout for fan-out"); connected_outputs_.insert(port_key); auto& in_ch = dst->template input_channel(); src->template set_output_channel(&in_ch); // Register channel probe for diagnostics std::string ch_name = src_name + ":" + std::to_string(SrcIdx) + " → " + dst_name + ":" + std::to_string(DstIdx); channel_probes_.push_back( std::make_unique>(in_ch, ch_name)); adj_[src_name].push_back(dst_name); return *this; } template Network& expose_input(std::string boundary_name, InputPort) { exposed_inputs_[boundary_name] = boundary_name; return *this; } template Network& expose_output(std::string boundary_name, OutputPort) { exposed_outputs_[boundary_name] = boundary_name; return *this; } Network& build() { topo_.clear(); std::map color; for (auto& [name, _] : nodes_) if (color[name] == 0) dfs(name, color); return *this; } // ── INode ───────────────────────────────────────────────────────────────── void start() override { start_time_ = clock_t::now(); for (auto& name : topo_) nodes_.at(name)->start(); start_watchdog(); #ifdef KPN_WEB_DEBUG web_server_ = std::make_unique( web_debug_port_, [this]() { auto s = collect_snapshots(); return web_debug::to_json(s.nodes, s.channels, {}, s.elapsed_s); }); web_server_->start(); std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n"; #endif } void stop() override { #ifdef KPN_WEB_DEBUG if (web_server_) web_server_->stop(); #endif stop_watchdog(); for (auto it = topo_.rbegin(); it != topo_.rend(); ++it) nodes_.at(*it)->stop(); } bool running() const override { return watchdog_.joinable(); } void set_name(std::string) override {} const NodeStats& stats() const override { static NodeStats dummy; return dummy; } NodeSnapshot node_snapshot(const std::string& name, double) const override { return {name, 0, 0, 0, 0, 0, 0, 0}; } // ── Configuration ───────────────────────────────────────────────────────── void set_watchdog_interval(std::chrono::milliseconds interval) { watchdog_interval_ = interval; } void set_error_handler(ErrorHandler h) { error_handler_ = std::move(h); } void set_diagnostics_handler(DiagnosticsHandler h) { diag_handler_ = std::move(h); } #ifdef KPN_WEB_DEBUG void set_web_debug_port(uint16_t port) { web_debug_port_ = port; } #endif // Print a diagnostics report to a stream (default: stderr). // Can be called at any time; thread-safe (reads atomics with relaxed ordering). void print_diagnostics(std::ostream& os = std::cerr) const { auto s = collect_snapshots(); os << format_report(s.nodes, s.channels, s.elapsed_s); } private: // ── Diagnostics collection ──────────────────────────────────────────────── struct Snapshots { std::vector nodes; std::vector channels; double elapsed_s; }; Snapshots collect_snapshots() const { double elapsed_s = std::chrono::duration( clock_t::now() - start_time_).count(); std::vector nodes; for (auto& name : topo_) nodes.push_back(nodes_.at(name)->node_snapshot(name, elapsed_s)); std::vector channels; for (auto& probe : channel_probes_) channels.push_back(probe->snapshot()); return {std::move(nodes), std::move(channels), elapsed_s}; } static std::string format_report(const std::vector& nodes, const std::vector& channels, double elapsed_s = 0.0) { std::ostringstream os; os << std::fixed << std::setprecision(1); os << "\n┌─ KPN++ Diagnostics ────────────────────────────────────────────────────────\n"; // Node table os << "│ Nodes:\n"; os << "│ " << std::left << std::setw(16) << "name" << std::setw(10) << "frames" << std::setw(12) << "exec ms" << std::setw(12) << "max ms" << std::setw(14) << "blocked ms" << std::setw(10) << "fps" << std::setw(12) << "cpu ms" << std::setw(10) << "util%" << "\n│ " << std::string(92, '-') << "\n"; for (auto& n : nodes) { os << "│ " << std::left << std::setw(16) << n.name << std::setw(10) << n.frames_processed << std::setw(12) << n.ema_exec_ms << std::setw(12) << n.max_exec_ms << std::setw(14) << n.total_blocked_ms << std::setw(10) << n.throughput_fps << std::setw(12) << n.total_cpu_ms << std::setw(10) << n.cpu_util_pct << "\n"; } // Channel table os << "│\n│ Channels:\n"; os << "│ " << std::left << std::setw(40) << "edge" << std::setw(8) << "fill%" << std::setw(8) << "peak%" << std::setw(8) << "pushes" << std::setw(8) << "drops" << std::setw(8) << "oflow" << std::setw(12) << "MB/s" << std::setw(10) << "item B" << "\n│ " << std::string(102, '-') << "\n"; for (auto& c : channels) { std::string flag = c.fill_pct() >= 80.0 ? " <<<" : c.peak_pct() >= 80.0 ? " (peak)" : ""; os << "│ " << std::left << std::setw(40) << c.name << std::setw(8) << c.fill_pct() << std::setw(8) << c.peak_pct() << std::setw(8) << c.pushes << std::setw(8) << c.drops << std::setw(8) << c.overflows << std::setw(12) << c.bandwidth_mbs(elapsed_s) << std::setw(10) << c.item_bytes << flag << "\n"; } // Bottleneck hint: node with highest ema_exec_ms if (!nodes.empty()) { auto it = std::max_element(nodes.begin(), nodes.end(), [](const NodeSnapshot& a, const NodeSnapshot& b) { return a.ema_exec_ms < b.ema_exec_ms; }); os << "│\n│ Bottleneck hint: '" << it->name << "' (avg exec " << it->ema_exec_ms << " ms)\n"; } os << "└────────────────────────────────────────────────────────────────\n"; return os.str(); } // ── Cycle detection / topological sort ─────────────────────────────────── void dfs(const std::string& name, std::map& color) { color[name] = 1; for (auto& nbr : adj_[name]) { if (color[nbr] == 1) throw NetworkCycleError{}; if (color[nbr] == 0) dfs(nbr, color); } color[name] = 2; topo_.insert(topo_.begin(), name); } // ── Watchdog ────────────────────────────────────────────────────────────── void start_watchdog() { watchdog_ = std::jthread([this](std::stop_token tok) { while (!tok.stop_requested()) { std::this_thread::sleep_for(watchdog_interval_); if (tok.stop_requested()) break; auto s = collect_snapshots(); if (diag_handler_) { diag_handler_(s.nodes, s.channels); } else { std::cerr << format_report(s.nodes, s.channels, s.elapsed_s); } } }); } void stop_watchdog() { if (watchdog_.joinable()) watchdog_.request_stop(), watchdog_.join(); } // ── State ───────────────────────────────────────────────────────────────── std::map nodes_; std::map> adj_; std::vector topo_; std::map exposed_inputs_; std::map exposed_outputs_; std::set> connected_outputs_; std::vector> channel_probes_; ErrorHandler error_handler_; DiagnosticsHandler diag_handler_; std::chrono::milliseconds watchdog_interval_{3000}; std::jthread watchdog_; clock_t::time_point start_time_; #ifdef KPN_WEB_DEBUG uint16_t web_debug_port_{9090}; std::unique_ptr web_server_; #endif }; } // namespace kpn