#pragma once // Only compiled when KPN_WEB_DEBUG is defined. network.hpp includes this conditionally. #include "diagnostics.hpp" #include #include #include #include #include #include namespace kpn::web_debug { // ── Minimal JSON serialiser ─────────────────────────────────────────────────── static std::string escape_json(const std::string& s) { std::string out; out.reserve(s.size()); for (char c : s) { if (c == '"') out += "\\\""; else if (c == '\\') out += "\\\\"; else if (c == '\n') out += "\\n"; else if (c == '\r') out += "\\r"; else if (c == '\t') out += "\\t"; else out += c; } return out; } // Parse "src_name:N → dst_name:M" into {src_name, dst_name}. // Stored separately so the browser doesn't need to regex-parse a UTF-8 arrow. static std::pair parse_edge_name(const std::string& name) { // Format: "::" auto arrow = name.find(" \xe2\x86\x92 "); // UTF-8 for → if (arrow == std::string::npos) return {name, name}; std::string src_part = name.substr(0, arrow); std::string dst_part = name.substr(arrow + 5); // " → " = 5 bytes (space + 3-byte arrow + space) // Strip ":N" index suffix auto sc1 = src_part.rfind(':'); auto sc2 = dst_part.rfind(':'); if (sc1 != std::string::npos) src_part = src_part.substr(0, sc1); if (sc2 != std::string::npos) dst_part = dst_part.substr(0, sc2); return {src_part, dst_part}; } static std::string to_json(const std::vector& nodes, const std::vector& channels, const std::vector& resources = {}, double elapsed_s = 0.0, const std::vector& pools = {}) { std::ostringstream o; o << std::fixed; o.precision(2); o << "{\"nodes\":["; for (std::size_t i = 0; i < nodes.size(); ++i) { const auto& n = nodes[i]; if (i) o << ','; o << "{\"id\":\"" << escape_json(n.name) << "\"" << ",\"frames\":" << n.frames_processed << ",\"ema_exec_ms\":" << n.ema_exec_ms << ",\"max_exec_ms\":" << n.max_exec_ms << ",\"blocked_ms\":" << n.total_blocked_ms << ",\"fps\":" << n.throughput_fps << ",\"total_cpu_ms\":" << n.total_cpu_ms << ",\"cpu_util_pct\":" << n.cpu_util_pct << "}"; } o << "],\"edges\":["; for (std::size_t i = 0; i < channels.size(); ++i) { const auto& c = channels[i]; if (i) o << ','; auto [src, dst] = parse_edge_name(c.name); o << "{\"name\":\"" << escape_json(c.name) << "\"" << ",\"source\":\"" << escape_json(src) << "\"" << ",\"target\":\"" << escape_json(dst) << "\"" << ",\"capacity\":" << c.capacity << ",\"current\":" << c.current_fill << ",\"fill_pct\":" << c.fill_pct() << ",\"peak_pct\":" << c.peak_pct() << ",\"pushes\":" << c.pushes << ",\"drops\":" << c.drops << ",\"overflows\":" << c.overflows << ",\"item_bytes\":" << c.item_bytes << ",\"bw_mbs\":" << c.bandwidth_mbs(elapsed_s) << "}"; } o << "],\"resources\":["; for (std::size_t i = 0; i < resources.size(); ++i) { const auto& r = resources[i]; if (i) o << ','; o << "{\"name\":\"" << escape_json(r.name) << "\"" << ",\"acquisitions\":" << r.acquisitions << ",\"avg_wait_ms\":" << r.avg_wait_ms << ",\"peak_waiters\":" << r.peak_waiters << ",\"current_waiters\":" << r.current_waiters << ",\"held\":" << (r.held ? "true" : "false") << "}"; } o << "],\"pools\":["; for (std::size_t i = 0; i < pools.size(); ++i) { const auto& p = pools[i]; if (i) o << ','; double in_rate = elapsed_s > 0.0 ? p.tasks_submitted / elapsed_s : 0.0; double out_rate = elapsed_s > 0.0 ? p.tasks_completed / elapsed_s : 0.0; o << "{\"name\":\"" << escape_json(p.name) << "\"" << ",\"thread_count\":" << p.thread_count << ",\"queue_depth\":" << p.queue_depth << ",\"active_count\":" << p.active_count << ",\"in_rate\":" << in_rate << ",\"out_rate\":" << out_rate << "}"; } o << "]}"; return o.str(); } // ── Embedded single-page HTML ───────────────────────────────────────────────── static const char* HTML = R"html( KPN++ Web Debug

Shared Resources

)html"; // ── WebDebugServer ──────────────────────────────────────────────────────────── class WebDebugServer { public: using SnapshotFn = std::function; explicit WebDebugServer(uint16_t port, SnapshotFn fn, const char* custom_html = nullptr) : port_(port), snapshot_fn_(std::move(fn)) , html_(custom_html ? custom_html : HTML) {} void start() { svr_.Get("/", [this](const httplib::Request&, httplib::Response& res) { res.set_content(html_, "text/html"); }); svr_.Get("/api/snapshot", [this](const httplib::Request&, httplib::Response& res) { res.set_content(snapshot_fn_(), "application/json"); }); thread_ = std::thread([this] { svr_.listen("0.0.0.0", static_cast(port_)); }); } void stop() { svr_.stop(); if (thread_.joinable()) thread_.join(); } ~WebDebugServer() { stop(); } WebDebugServer(const WebDebugServer&) = delete; WebDebugServer& operator=(const WebDebugServer&) = delete; private: uint16_t port_; SnapshotFn snapshot_fn_; const char* html_; httplib::Server svr_; std::thread thread_; }; } // namespace kpn::web_debug