Add a unified observability interface for applications with multiple networks
All checks were successful
🧪 Test / test (push) Successful in 6m9s
All checks were successful
🧪 Test / test (push) Successful in 6m9s
This commit is contained in:
parent
278c122e8f
commit
1e9ba5ee66
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
|||||||
# Build output
|
# Build output
|
||||||
build/
|
build/
|
||||||
|
build_debug/
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
159
examples/14_debug_hub/main.cpp
Normal file
159
examples/14_debug_hub/main.cpp
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// Example 14 — DebugHub with Shared Resource Token
|
||||||
|
//
|
||||||
|
// Two independent KPN networks compete for one shared inference resource
|
||||||
|
// (simulating a small GPU or single-session ONNX runtime). The DebugHub
|
||||||
|
// serves a single web UI at http://localhost:9090 with:
|
||||||
|
//
|
||||||
|
// [All Networks] — resource utilisation cards + cross-network node table
|
||||||
|
// [detect] — force-directed graph for the detection pipeline
|
||||||
|
// [classify] — force-directed graph for the classification pipeline
|
||||||
|
//
|
||||||
|
// Topology:
|
||||||
|
//
|
||||||
|
// detect pipeline:
|
||||||
|
// [source_detect] ──> [run_detect] ──> [sink_detect]
|
||||||
|
//
|
||||||
|
// classify pipeline:
|
||||||
|
// [source_classify] ──> [run_classify] ──> [sink_classify]
|
||||||
|
//
|
||||||
|
// Both [run_detect] and [run_classify] call gpu.acquire() before touching the
|
||||||
|
// simulated device. The priority-based token awards the next slot to the
|
||||||
|
// waiter that is more likely to make useful progress (higher priority score).
|
||||||
|
//
|
||||||
|
// Build: cmake -DKPN_WEB_DEBUG=ON .. && cmake --build .
|
||||||
|
// Run: ./14_debug_hub
|
||||||
|
// UI: http://localhost:9090
|
||||||
|
|
||||||
|
#ifdef KPN_WEB_DEBUG
|
||||||
|
|
||||||
|
#include <kpn/kpn.hpp>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
using namespace kpn;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
// ── Simulated inference device ────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Represents any exclusive, serialised accelerator: GPU session, ONNX runtime,
|
||||||
|
// hardware encoder, etc. Only one caller can hold it at a time.
|
||||||
|
|
||||||
|
struct GPU {
|
||||||
|
// Detection model: fast, 8 ms per frame.
|
||||||
|
int detect(int frame_id) {
|
||||||
|
std::this_thread::sleep_for(8ms);
|
||||||
|
return frame_id * 2; // synthetic "score"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classification model: heavier, 14 ms per frame.
|
||||||
|
int classify(int frame_id) {
|
||||||
|
std::this_thread::sleep_for(14ms);
|
||||||
|
return frame_id % 10; // synthetic "label"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global pointer so free-function nodes can reach the resource.
|
||||||
|
// In production code, capture by reference inside an ObjectNode functor instead.
|
||||||
|
static SharedResource<GPU>* g_gpu = nullptr;
|
||||||
|
|
||||||
|
// ── Detection pipeline ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static int source_detect() {
|
||||||
|
static std::atomic<int> id{0};
|
||||||
|
std::this_thread::sleep_for(25ms); // ~40 fps source rate
|
||||||
|
return id.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int run_detect(int frame_id) {
|
||||||
|
// Higher priority: detection is latency-critical.
|
||||||
|
auto guard = g_gpu->acquire([] { return 0.7f; });
|
||||||
|
return guard->detect(frame_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic<uint64_t> detect_out{0};
|
||||||
|
static void sink_detect(int) {
|
||||||
|
detect_out.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Classification pipeline ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static int source_classify() {
|
||||||
|
static std::atomic<int> id{0};
|
||||||
|
std::this_thread::sleep_for(40ms); // ~25 fps source rate
|
||||||
|
return id.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int run_classify(int frame_id) {
|
||||||
|
// Lower priority: classification is best-effort.
|
||||||
|
auto guard = g_gpu->acquire([] { return 0.3f; });
|
||||||
|
return guard->classify(frame_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::atomic<uint64_t> classify_out{0};
|
||||||
|
static void sink_classify(int) {
|
||||||
|
classify_out.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
SharedResource<GPU> gpu;
|
||||||
|
g_gpu = &gpu;
|
||||||
|
|
||||||
|
// ── Detection network ─────────────────────────────────────────────────────
|
||||||
|
auto src_det = make_node<source_detect, "source_detect">(4);
|
||||||
|
auto inf_det = make_node<run_detect, "run_detect" >(4);
|
||||||
|
auto snk_det = make_node<sink_detect, "sink_detect" >(4);
|
||||||
|
|
||||||
|
auto net_detect = make_network(
|
||||||
|
edge(src_det.output<0>(), inf_det.input<0>()),
|
||||||
|
edge(inf_det.output<0>(), snk_det.input<0>())
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Classification network ────────────────────────────────────────────────
|
||||||
|
auto src_cls = make_node<source_classify, "source_classify">(4);
|
||||||
|
auto inf_cls = make_node<run_classify, "run_classify" >(4);
|
||||||
|
auto snk_cls = make_node<sink_classify, "sink_classify" >(4);
|
||||||
|
|
||||||
|
auto net_classify = make_network(
|
||||||
|
edge(src_cls.output<0>(), inf_cls.input<0>()),
|
||||||
|
edge(inf_cls.output<0>(), snk_cls.input<0>())
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Hub — one debug server for both networks + the shared resource ─────────
|
||||||
|
DebugHub hub(9090);
|
||||||
|
hub.register_network("detect", net_detect);
|
||||||
|
hub.register_network("classify", net_classify);
|
||||||
|
hub.register_resource("gpu", &gpu);
|
||||||
|
|
||||||
|
net_detect.start();
|
||||||
|
net_classify.start();
|
||||||
|
hub.start();
|
||||||
|
|
||||||
|
std::cout << "Running — open http://localhost:9090\n"
|
||||||
|
<< "Tabs: [All Networks] [detect] [classify]\n"
|
||||||
|
<< "Press Enter to stop.\n";
|
||||||
|
std::cin.get();
|
||||||
|
|
||||||
|
net_detect.stop();
|
||||||
|
net_classify.stop();
|
||||||
|
|
||||||
|
std::cout << "\nResults:\n"
|
||||||
|
<< " detect: " << detect_out.load() << " frames\n"
|
||||||
|
<< " classify: " << classify_out.load() << " frames\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else // no KPN_WEB_DEBUG
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
int main() {
|
||||||
|
std::cerr << "This example requires KPN_WEB_DEBUG.\n"
|
||||||
|
<< "Rebuild with: cmake -DKPN_WEB_DEBUG=ON ..\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -15,6 +15,10 @@ kpn_example(10_static_hello_pipeline)
|
|||||||
kpn_example(11_static_fanout)
|
kpn_example(11_static_fanout)
|
||||||
if(KPN_WEB_DEBUG)
|
if(KPN_WEB_DEBUG)
|
||||||
kpn_target_enable_web_debug(06_watchdog)
|
kpn_target_enable_web_debug(06_watchdog)
|
||||||
|
|
||||||
|
add_executable(14_debug_hub 14_debug_hub/main.cpp)
|
||||||
|
target_link_libraries(14_debug_hub PRIVATE kpn)
|
||||||
|
kpn_target_enable_web_debug(14_debug_hub)
|
||||||
endif()
|
endif()
|
||||||
# 07 and 08 require the Python bindings — only add if built
|
# 07 and 08 require the Python bindings — only add if built
|
||||||
if(KPN_BUILD_PYTHON)
|
if(KPN_BUILD_PYTHON)
|
||||||
|
|||||||
456
include/kpn/debug_hub.hpp
Normal file
456
include/kpn/debug_hub.hpp
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
#pragma once
|
||||||
|
// Only active when KPN_WEB_DEBUG is defined.
|
||||||
|
|
||||||
|
#ifdef KPN_WEB_DEBUG
|
||||||
|
#include "diagnostics.hpp"
|
||||||
|
#include "web_debug.hpp"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kpn {
|
||||||
|
|
||||||
|
// ── Hub HTML ──────────────────────────────────────────────────────────────────
|
||||||
|
// Multi-tab UI: one tab per registered network (force-directed graph) +
|
||||||
|
// an "All Networks" tab showing shared resource cards and a cross-network
|
||||||
|
// node table.
|
||||||
|
|
||||||
|
static const char* HUB_HTML = R"html(<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>KPN++ Debug Hub</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;background:#1a1a2e;color:#eee;font-family:monospace}
|
||||||
|
#hdr{display:flex;align-items:center;padding:0 16px;background:#16213e;
|
||||||
|
border-bottom:1px solid #0f3460;height:44px;gap:8px;overflow-x:auto}
|
||||||
|
#hdr h1{margin:0;font-size:16px;color:#e94560;white-space:nowrap;margin-right:12px}
|
||||||
|
#tab-bar{display:flex;gap:2px;flex:1}
|
||||||
|
.tab{padding:4px 14px;border:none;background:#0f3460;color:#aaa;
|
||||||
|
cursor:pointer;font-family:monospace;font-size:11px;border-radius:2px;white-space:nowrap}
|
||||||
|
.tab.active{background:#e94560;color:#fff}
|
||||||
|
.tab:hover:not(.active){background:#1e3a6e;color:#eee}
|
||||||
|
#status{font-size:10px;color:#555;white-space:nowrap}
|
||||||
|
|
||||||
|
.panel{display:none}
|
||||||
|
.panel.active{display:block}
|
||||||
|
|
||||||
|
/* ── All Networks tab ─────────────────────────────────────── */
|
||||||
|
#panel-all{height:calc(100vh - 44px);overflow-y:auto;padding:16px;
|
||||||
|
display:none;gap:16px;grid-template-columns:300px 1fr}
|
||||||
|
#panel-all.active{display:grid;align-content:start}
|
||||||
|
#panel-all h2{font-size:11px;color:#e94560;margin:0 0 8px;
|
||||||
|
letter-spacing:1px;text-transform:uppercase}
|
||||||
|
|
||||||
|
#res-col{grid-column:1}
|
||||||
|
.res-card{background:#16213e;border:1px solid #0f3460;border-radius:4px;
|
||||||
|
padding:10px 12px;margin-bottom:8px}
|
||||||
|
.res-head{display:flex;justify-content:space-between;align-items:center}
|
||||||
|
.res-name{font-size:12px}
|
||||||
|
.badge{font-size:9px;padding:2px 6px;border-radius:2px}
|
||||||
|
.held{background:#e94560}.free{background:#4CAF50;color:#111}
|
||||||
|
.res-meta{font-size:9px;color:#666;margin-top:5px;display:flex;gap:12px;flex-wrap:wrap}
|
||||||
|
.bar-wrap{height:3px;background:#0f3460;border-radius:2px;margin-top:7px}
|
||||||
|
.bar{height:3px;border-radius:2px;transition:width 0.4s}
|
||||||
|
|
||||||
|
#nodes-col{grid-column:2;overflow-y:auto;max-height:calc(100vh - 76px)}
|
||||||
|
table{width:100%;border-collapse:collapse;font-size:10px}
|
||||||
|
th{padding:4px 8px;color:#555;border-bottom:1px solid #0f3460;text-align:left;
|
||||||
|
position:sticky;top:0;background:#1a1a2e;z-index:1}
|
||||||
|
td{padding:2px 8px;border-bottom:1px solid #16213e}
|
||||||
|
tr:hover td{background:#16213e}
|
||||||
|
.ntag{font-size:9px;background:#0f3460;padding:1px 4px;border-radius:2px;color:#4CAF50}
|
||||||
|
|
||||||
|
/* ── Per-network graph panels ─────────────────────────────── */
|
||||||
|
.graph-panel{width:100vw;height:calc(100vh - 44px)}
|
||||||
|
svg.net{width:100%;height:100%}
|
||||||
|
.node circle{stroke:#fff;stroke-width:1.5px}
|
||||||
|
.node text{font-size:11px;fill:#eee;pointer-events:none;text-anchor:middle}
|
||||||
|
.node .st{font-size:9px;fill:#aaa}
|
||||||
|
.link{fill:none;stroke-width:2px}
|
||||||
|
.lbl{font-size:9px;fill:#ccc}
|
||||||
|
#tip{position:absolute;background:#0f3460;border:1px solid #e94560;border-radius:4px;
|
||||||
|
padding:8px 12px;font-size:11px;pointer-events:none;display:none;
|
||||||
|
white-space:pre;line-height:1.6}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="hdr">
|
||||||
|
<h1>KPN++ Debug Hub</h1>
|
||||||
|
<div id="tab-bar"></div>
|
||||||
|
<span id="status">connecting…</span>
|
||||||
|
</div>
|
||||||
|
<div id="panels">
|
||||||
|
<div id="panel-all" class="panel"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tip"></div>
|
||||||
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const R = 28;
|
||||||
|
const tip = d3.select('#tip');
|
||||||
|
|
||||||
|
const nc = ema => ema>100?'#e94560':ema>50?'#e07040':ema>10?'#f0c040':'#4CAF50';
|
||||||
|
const ec = pct => pct>=80?'#e94560':pct>=50?'#f0c040':'#4CAF50';
|
||||||
|
const ea = pct => pct>=80?'url(#a2)':pct>=50?'url(#a1)':'url(#a0)';
|
||||||
|
|
||||||
|
// ── Tab management ────────────────────────────────────────────────────────────
|
||||||
|
let activeTab = null;
|
||||||
|
|
||||||
|
function ensureTab(id, label) {
|
||||||
|
if (document.getElementById('tab-' + id)) return;
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.className = 'tab'; b.id = 'tab-' + id; b.textContent = label;
|
||||||
|
b.onclick = () => showTab(id);
|
||||||
|
document.getElementById('tab-bar').appendChild(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTab(id) {
|
||||||
|
activeTab = id;
|
||||||
|
document.querySelectorAll('.tab').forEach(b =>
|
||||||
|
b.classList.toggle('active', b.id === 'tab-' + id));
|
||||||
|
document.querySelectorAll('.panel').forEach(p =>
|
||||||
|
p.classList.toggle('active', p.id === 'panel-' + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── All Networks tab ──────────────────────────────────────────────────────────
|
||||||
|
function renderAll(data) {
|
||||||
|
const panel = document.getElementById('panel-all');
|
||||||
|
|
||||||
|
// Resources column
|
||||||
|
let rhtml = '<div id="res-col"><h2>Shared Resources</h2>';
|
||||||
|
if (!data.resources || !data.resources.length)
|
||||||
|
rhtml += '<div style="color:#444;font-size:11px">None registered</div>';
|
||||||
|
for (const r of (data.resources || [])) {
|
||||||
|
const wpct = Math.min(100, r.avg_wait_ms).toFixed(1);
|
||||||
|
const bc = r.current_waiters > 0 ? '#e94560' : '#4CAF50';
|
||||||
|
rhtml += `<div class="res-card">
|
||||||
|
<div class="res-head">
|
||||||
|
<span class="res-name">${r.name}</span>
|
||||||
|
<span class="badge ${r.held ? 'held' : 'free'}">${r.held ? 'HELD' : 'free'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="res-meta">
|
||||||
|
<span>avg wait ${r.avg_wait_ms.toFixed(1)} ms</span>
|
||||||
|
<span>waiters ${r.current_waiters} / peak ${r.peak_waiters}</span>
|
||||||
|
<span>${r.acquisitions} acq</span>
|
||||||
|
</div>
|
||||||
|
<div class="bar-wrap">
|
||||||
|
<div class="bar" style="width:${wpct}%;background:${bc}"></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
rhtml += '</div>';
|
||||||
|
|
||||||
|
// Nodes column — all networks in one table
|
||||||
|
let rows = '';
|
||||||
|
for (const net of data.networks) {
|
||||||
|
for (const n of net.nodes) {
|
||||||
|
rows += `<tr>
|
||||||
|
<td><span class="ntag">${net.name}</span></td>
|
||||||
|
<td>${n.id}</td>
|
||||||
|
<td>${n.fps.toFixed(1)}</td>
|
||||||
|
<td>${n.ema_exec_ms.toFixed(2)}</td>
|
||||||
|
<td>${n.max_exec_ms.toFixed(2)}</td>
|
||||||
|
<td>${n.blocked_ms.toFixed(2)}</td>
|
||||||
|
<td>${n.cpu_util_pct.toFixed(1)}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const thtml = `<div id="nodes-col"><h2>All Nodes</h2>
|
||||||
|
<table>
|
||||||
|
<tr><th>Network</th><th>Node</th><th>fps</th>
|
||||||
|
<th>exec ema (ms)</th><th>exec max (ms)</th>
|
||||||
|
<th>blocked (ms)</th><th>cpu %</th></tr>
|
||||||
|
${rows}
|
||||||
|
</table></div>`;
|
||||||
|
|
||||||
|
panel.innerHTML = rhtml + thtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-network graph ─────────────────────────────────────────────────────────
|
||||||
|
const nets = {};
|
||||||
|
|
||||||
|
function initNet(netData) {
|
||||||
|
const name = netData.name;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.id = 'panel-' + name;
|
||||||
|
div.className = 'panel graph-panel';
|
||||||
|
document.getElementById('panels').appendChild(div);
|
||||||
|
|
||||||
|
const svg = d3.select(div).append('svg').attr('class', 'net');
|
||||||
|
const W = () => div.clientWidth || window.innerWidth;
|
||||||
|
const H = () => div.clientHeight || (window.innerHeight - 44);
|
||||||
|
|
||||||
|
const defs = svg.append('defs');
|
||||||
|
['#4CAF50','#f0c040','#e94560'].forEach((col, i) =>
|
||||||
|
defs.append('marker').attr('id','a'+i)
|
||||||
|
.attr('viewBox','0 -5 10 10').attr('refX',10).attr('refY',0)
|
||||||
|
.attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto')
|
||||||
|
.append('path').attr('d','M0,-5L10,0L0,5').attr('fill',col));
|
||||||
|
|
||||||
|
const g = svg.append('g');
|
||||||
|
svg.call(d3.zoom().on('zoom', e => g.attr('transform', e.transform)));
|
||||||
|
|
||||||
|
const nodes = netData.nodes.map(n => ({...n, x: W()/2, y: H()/2}));
|
||||||
|
const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
|
||||||
|
const links = netData.edges
|
||||||
|
.map(e => ({...e, source: byId[e.source], target: byId[e.target]}))
|
||||||
|
.filter(e => e.source && e.target);
|
||||||
|
|
||||||
|
const sim = d3.forceSimulation(nodes)
|
||||||
|
.force('link', d3.forceLink(links).distance(150).strength(0.5))
|
||||||
|
.force('charge', d3.forceManyBody().strength(-350))
|
||||||
|
.force('center', d3.forceCenter(W()/2, H()/2))
|
||||||
|
.force('collide', d3.forceCollide(R + 18))
|
||||||
|
.on('tick', tick);
|
||||||
|
|
||||||
|
const lsel = g.append('g').selectAll('line').data(links).join('line')
|
||||||
|
.attr('class','link')
|
||||||
|
.attr('stroke', d => ec(d.fill_pct))
|
||||||
|
.attr('marker-end', d => ea(d.fill_pct));
|
||||||
|
|
||||||
|
const llbl = g.append('g').selectAll('text').data(links).join('text')
|
||||||
|
.attr('class','lbl').text(d => `${d.fill_pct.toFixed(0)}%`);
|
||||||
|
|
||||||
|
const ng = g.append('g').selectAll('g').data(nodes).join('g').attr('class','node')
|
||||||
|
.call(d3.drag()
|
||||||
|
.on('start',(e,d)=>{ if(!e.active) sim.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
|
||||||
|
.on('drag', (e,d)=>{ d.fx=e.x; d.fy=e.y; })
|
||||||
|
.on('end', (e,d)=>{ if(!e.active) sim.alphaTarget(0); d.fx=null; d.fy=null; }));
|
||||||
|
|
||||||
|
ng.append('circle').attr('r', R).attr('fill', d => nc(d.ema_exec_ms));
|
||||||
|
ng.append('text').attr('dy', 4).text(d => d.id);
|
||||||
|
ng.append('text').attr('class','st').attr('dy', 18)
|
||||||
|
.text(d => `${d.ema_exec_ms.toFixed(1)}ms ${d.fps.toFixed(1)}fps`);
|
||||||
|
|
||||||
|
ng.on('mousemove', (e,d) =>
|
||||||
|
tip.style('display','block')
|
||||||
|
.style('left',(e.pageX+12)+'px').style('top',(e.pageY+12)+'px')
|
||||||
|
.text(`${d.id}\nframes: ${d.frames} fps: ${d.fps.toFixed(2)}\n` +
|
||||||
|
`exec ema: ${d.ema_exec_ms.toFixed(2)}ms max: ${d.max_exec_ms.toFixed(2)}ms\n` +
|
||||||
|
`blocked: ${d.blocked_ms.toFixed(2)}ms cpu: ${d.cpu_util_pct.toFixed(1)}%`))
|
||||||
|
.on('mouseleave', () => tip.style('display','none'));
|
||||||
|
|
||||||
|
g.selectAll('.link')
|
||||||
|
.on('mousemove', (e,d) =>
|
||||||
|
tip.style('display','block')
|
||||||
|
.style('left',(e.pageX+12)+'px').style('top',(e.pageY+12)+'px')
|
||||||
|
.text(`${d.name}\nfill: ${d.fill_pct.toFixed(1)}% peak: ${d.peak_pct.toFixed(1)}%\n` +
|
||||||
|
`cap: ${d.capacity} pushes: ${d.pushes} drops: ${d.drops}\n` +
|
||||||
|
`bandwidth: ${(d.bw_mbs||0).toFixed(2)} MB/s`))
|
||||||
|
.on('mouseleave', () => tip.style('display','none'));
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
const w = W(), h = H();
|
||||||
|
nodes.forEach(d => {
|
||||||
|
d.x = Math.max(R, Math.min(w - R, d.x));
|
||||||
|
d.y = Math.max(R, Math.min(h - R, d.y));
|
||||||
|
});
|
||||||
|
lsel
|
||||||
|
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
||||||
|
.attr('x2', d => { const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y,
|
||||||
|
dist=Math.sqrt(dx*dx+dy*dy)||1;
|
||||||
|
return d.target.x-(dx/dist)*(R+8); })
|
||||||
|
.attr('y2', d => { const dx=d.target.x-d.source.x, dy=d.target.y-d.source.y,
|
||||||
|
dist=Math.sqrt(dx*dx+dy*dy)||1;
|
||||||
|
return d.target.y-(dy/dist)*(R+8); });
|
||||||
|
llbl.attr('x', d => (d.source.x+d.target.x)/2)
|
||||||
|
.attr('y', d => (d.source.y+d.target.y)/2 - 6);
|
||||||
|
ng.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
nets[name] = {nodes, links, sim, ng, lsel, llbl};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNet(netData) {
|
||||||
|
const st = nets[netData.name];
|
||||||
|
if (!st) return;
|
||||||
|
const byId = Object.fromEntries(netData.nodes.map(n => [n.id, n]));
|
||||||
|
st.nodes.forEach(n => {
|
||||||
|
const f = byId[n.id];
|
||||||
|
if (f) Object.assign(n, {frames:f.frames, ema_exec_ms:f.ema_exec_ms,
|
||||||
|
max_exec_ms:f.max_exec_ms, blocked_ms:f.blocked_ms, fps:f.fps,
|
||||||
|
total_cpu_ms:f.total_cpu_ms, cpu_util_pct:f.cpu_util_pct});
|
||||||
|
});
|
||||||
|
netData.edges.forEach((e,i) => {
|
||||||
|
if (st.links[i]) Object.assign(st.links[i], {fill_pct:e.fill_pct,
|
||||||
|
peak_pct:e.peak_pct, pushes:e.pushes, drops:e.drops,
|
||||||
|
overflows:e.overflows, current:e.current, bw_mbs:e.bw_mbs});
|
||||||
|
});
|
||||||
|
st.ng.select('circle').attr('fill', d => nc(d.ema_exec_ms));
|
||||||
|
st.ng.select('.st').text(d => `${d.ema_exec_ms.toFixed(1)}ms ${d.fps.toFixed(1)}fps`);
|
||||||
|
st.lsel.attr('stroke', d => ec(d.fill_pct)).attr('marker-end', d => ea(d.fill_pct));
|
||||||
|
st.llbl.text(d => `${d.fill_pct.toFixed(0)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Poll loop ─────────────────────────────────────────────────────────────────
|
||||||
|
let init = false;
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/snapshot');
|
||||||
|
if (!r.ok) throw new Error(r.status);
|
||||||
|
const data = await r.json();
|
||||||
|
|
||||||
|
if (!init) {
|
||||||
|
ensureTab('all', 'All Networks');
|
||||||
|
data.networks.forEach(net => ensureTab(net.name, net.name));
|
||||||
|
showTab('all');
|
||||||
|
data.networks.forEach(initNet);
|
||||||
|
init = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAll(data);
|
||||||
|
data.networks.forEach(updateNet);
|
||||||
|
|
||||||
|
document.getElementById('status').textContent =
|
||||||
|
`${new Date().toLocaleTimeString()} · ${data.networks.length} nets · ${(data.resources||[]).length} resources`;
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('status').textContent = 'error: ' + e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll();
|
||||||
|
setInterval(poll, 500);
|
||||||
|
window.addEventListener('resize', () =>
|
||||||
|
Object.values(nets).forEach(st =>
|
||||||
|
st.sim.force('center', d3.forceCenter(
|
||||||
|
(document.getElementById('panel-' + Object.keys(nets).find(k => nets[k] === st))?.clientWidth || window.innerWidth) / 2,
|
||||||
|
(document.getElementById('panel-' + Object.keys(nets).find(k => nets[k] === st))?.clientHeight || window.innerHeight - 44) / 2
|
||||||
|
)).alpha(0.1).restart()));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)html";
|
||||||
|
|
||||||
|
// ── DebugHub ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class DebugHub {
|
||||||
|
public:
|
||||||
|
explicit DebugHub(uint16_t port = 9090) : port_(port) {}
|
||||||
|
|
||||||
|
~DebugHub() { stop(); }
|
||||||
|
DebugHub(const DebugHub&) = delete;
|
||||||
|
DebugHub& operator=(const DebugHub&) = delete;
|
||||||
|
|
||||||
|
// Register a network. Disables that network's own web server so the hub
|
||||||
|
// becomes the single debug endpoint. Call before network.start().
|
||||||
|
template<typename Net>
|
||||||
|
void register_network(const std::string& name, Net& net) {
|
||||||
|
net.disable_web_server();
|
||||||
|
networks_.push_back({name, [&net, name] {
|
||||||
|
auto s = net.network_snapshot();
|
||||||
|
s.name = name;
|
||||||
|
return s;
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a shared resource — appears in the "All Networks" resource panel.
|
||||||
|
void register_resource(const std::string& name, IResourceProbe* probe) {
|
||||||
|
resources_.emplace_back(name, probe);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
server_ = std::make_unique<web_debug::WebDebugServer>(
|
||||||
|
port_,
|
||||||
|
[this] { return build_json(); },
|
||||||
|
HUB_HTML);
|
||||||
|
server_->start();
|
||||||
|
std::cerr << "[kpn] hub debug UI: http://localhost:" << port_ << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() { if (server_) server_->stop(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Serialise nodes array for one network snapshot
|
||||||
|
static void write_nodes(std::ostream& o, const NetworkSnapshot& s) {
|
||||||
|
o << "[";
|
||||||
|
for (std::size_t i = 0; i < s.nodes.size(); ++i) {
|
||||||
|
const auto& n = s.nodes[i];
|
||||||
|
if (i) o << ',';
|
||||||
|
o << "{\"id\":\"" << web_debug::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 << "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialise edges array for one network snapshot
|
||||||
|
static void write_edges(std::ostream& o, const NetworkSnapshot& s) {
|
||||||
|
o << "[";
|
||||||
|
for (std::size_t i = 0; i < s.channels.size(); ++i) {
|
||||||
|
const auto& c = s.channels[i];
|
||||||
|
if (i) o << ',';
|
||||||
|
auto [src, dst] = web_debug::parse_edge_name(c.name);
|
||||||
|
o << "{\"name\":\"" << web_debug::escape_json(c.name) << "\""
|
||||||
|
<< ",\"source\":\"" << web_debug::escape_json(src) << "\""
|
||||||
|
<< ",\"target\":\"" << web_debug::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(s.elapsed_s)
|
||||||
|
<< "}";
|
||||||
|
}
|
||||||
|
o << "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string build_json() const {
|
||||||
|
std::ostringstream o;
|
||||||
|
o << std::fixed;
|
||||||
|
o.precision(2);
|
||||||
|
|
||||||
|
o << "{\"networks\":[";
|
||||||
|
for (std::size_t i = 0; i < networks_.size(); ++i) {
|
||||||
|
if (i) o << ',';
|
||||||
|
auto s = networks_[i].fn();
|
||||||
|
o << "{\"name\":\"" << web_debug::escape_json(networks_[i].name) << "\""
|
||||||
|
<< ",\"nodes\":"; write_nodes(o, s);
|
||||||
|
o << ",\"edges\":"; write_edges(o, s);
|
||||||
|
o << "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
o << "],\"resources\":[";
|
||||||
|
for (std::size_t i = 0; i < resources_.size(); ++i) {
|
||||||
|
if (i) o << ',';
|
||||||
|
const auto r = resources_[i].second->snapshot(resources_[i].first);
|
||||||
|
o << "{\"name\":\"" << web_debug::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 << "]}";
|
||||||
|
return o.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
std::string name;
|
||||||
|
std::function<NetworkSnapshot()> fn;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint16_t port_;
|
||||||
|
std::vector<Entry> networks_;
|
||||||
|
std::vector<std::pair<std::string,IResourceProbe*>> resources_;
|
||||||
|
std::unique_ptr<web_debug::WebDebugServer> server_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kpn
|
||||||
|
#endif // KPN_WEB_DEBUG
|
||||||
@ -134,6 +134,15 @@ struct NodeSnapshot {
|
|||||||
double cpu_util_pct; // exec_ms / (exec_ms + blocked_ms) * 100
|
double cpu_util_pct; // exec_ms / (exec_ms + blocked_ms) * 100
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Cross-network snapshot (used by DebugHub) ─────────────────────────────────
|
||||||
|
|
||||||
|
struct NetworkSnapshot {
|
||||||
|
std::string name;
|
||||||
|
std::vector<NodeSnapshot> nodes;
|
||||||
|
std::vector<ChannelSnapshot> channels;
|
||||||
|
double elapsed_s;
|
||||||
|
};
|
||||||
|
|
||||||
// ── Resource statistics + snapshot ───────────────────────────────────────────
|
// ── Resource statistics + snapshot ───────────────────────────────────────────
|
||||||
|
|
||||||
struct ResourceSnapshot {
|
struct ResourceSnapshot {
|
||||||
|
|||||||
@ -9,5 +9,6 @@
|
|||||||
#include "fanout.hpp"
|
#include "fanout.hpp"
|
||||||
#include "shared_resource.hpp"
|
#include "shared_resource.hpp"
|
||||||
#include "static_network.hpp"
|
#include "static_network.hpp"
|
||||||
|
#include "debug_hub.hpp"
|
||||||
#include "main_thread_node.hpp"
|
#include "main_thread_node.hpp"
|
||||||
#include "network.hpp"
|
#include "network.hpp"
|
||||||
|
|||||||
@ -126,7 +126,7 @@ public:
|
|||||||
web_debug_port_,
|
web_debug_port_,
|
||||||
[this]() {
|
[this]() {
|
||||||
auto s = collect_snapshots();
|
auto s = collect_snapshots();
|
||||||
return web_debug::to_json(s.nodes, s.channels, s.elapsed_s);
|
return web_debug::to_json(s.nodes, s.channels, {}, s.elapsed_s);
|
||||||
});
|
});
|
||||||
web_server_->start();
|
web_server_->start();
|
||||||
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
|
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
|
||||||
|
|||||||
@ -110,14 +110,16 @@ public:
|
|||||||
for (auto* n : user_nodes_topo_) n->start();
|
for (auto* n : user_nodes_topo_) n->start();
|
||||||
for (auto* n : fanout_nodes_ptr_) n->start();
|
for (auto* n : fanout_nodes_ptr_) n->start();
|
||||||
#ifdef KPN_WEB_DEBUG
|
#ifdef KPN_WEB_DEBUG
|
||||||
web_server_ = std::make_unique<web_debug::WebDebugServer>(
|
if (web_server_enabled_) {
|
||||||
web_debug_port_,
|
web_server_ = std::make_unique<web_debug::WebDebugServer>(
|
||||||
[this]() {
|
web_debug_port_,
|
||||||
auto s = collect_snapshots();
|
[this]() {
|
||||||
return web_debug::to_json(s.nodes, s.channels, s.resources, s.elapsed_s);
|
auto s = collect_snapshots();
|
||||||
});
|
return web_debug::to_json(s.nodes, s.channels, s.resources, s.elapsed_s);
|
||||||
web_server_->start();
|
});
|
||||||
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
|
web_server_->start();
|
||||||
|
std::cerr << "[kpn] web debug UI: http://localhost:" << web_debug_port_ << "\n";
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,8 +144,16 @@ public:
|
|||||||
|
|
||||||
#ifdef KPN_WEB_DEBUG
|
#ifdef KPN_WEB_DEBUG
|
||||||
void set_web_debug_port(uint16_t port) { web_debug_port_ = port; }
|
void set_web_debug_port(uint16_t port) { web_debug_port_ = port; }
|
||||||
|
// Called by DebugHub::register_network() so the hub owns the debug server.
|
||||||
|
void disable_web_server() { web_server_enabled_ = false; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Returns a snapshot of this network's nodes and channels for the DebugHub.
|
||||||
|
NetworkSnapshot network_snapshot() const {
|
||||||
|
auto s = collect_snapshots();
|
||||||
|
return {"", std::move(s.nodes), std::move(s.channels), s.elapsed_s};
|
||||||
|
}
|
||||||
|
|
||||||
// Register a shared resource so it appears in diagnostics and the debug UI.
|
// Register a shared resource so it appears in diagnostics and the debug UI.
|
||||||
// The probe must outlive this network (typically the resource is on the same stack).
|
// The probe must outlive this network (typically the resource is on the same stack).
|
||||||
void register_resource(const std::string& name, IResourceProbe* probe) {
|
void register_resource(const std::string& name, IResourceProbe* probe) {
|
||||||
@ -205,6 +215,7 @@ private:
|
|||||||
clock_t::time_point start_time_;
|
clock_t::time_point start_time_;
|
||||||
#ifdef KPN_WEB_DEBUG
|
#ifdef KPN_WEB_DEBUG
|
||||||
uint16_t web_debug_port_{9090};
|
uint16_t web_debug_port_{9090};
|
||||||
|
bool web_server_enabled_{true};
|
||||||
std::unique_ptr<web_debug::WebDebugServer> web_server_;
|
std::unique_ptr<web_debug::WebDebugServer> web_server_;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@ -355,12 +355,14 @@ class WebDebugServer {
|
|||||||
public:
|
public:
|
||||||
using SnapshotFn = std::function<std::string()>;
|
using SnapshotFn = std::function<std::string()>;
|
||||||
|
|
||||||
explicit WebDebugServer(uint16_t port, SnapshotFn fn)
|
explicit WebDebugServer(uint16_t port, SnapshotFn fn,
|
||||||
: port_(port), snapshot_fn_(std::move(fn)) {}
|
const char* custom_html = nullptr)
|
||||||
|
: port_(port), snapshot_fn_(std::move(fn))
|
||||||
|
, html_(custom_html ? custom_html : HTML) {}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
svr_.Get("/", [](const httplib::Request&, httplib::Response& res) {
|
svr_.Get("/", [this](const httplib::Request&, httplib::Response& res) {
|
||||||
res.set_content(HTML, "text/html");
|
res.set_content(html_, "text/html");
|
||||||
});
|
});
|
||||||
svr_.Get("/api/snapshot", [this](const httplib::Request&, httplib::Response& res) {
|
svr_.Get("/api/snapshot", [this](const httplib::Request&, httplib::Response& res) {
|
||||||
res.set_content(snapshot_fn_(), "application/json");
|
res.set_content(snapshot_fn_(), "application/json");
|
||||||
@ -381,10 +383,11 @@ public:
|
|||||||
WebDebugServer& operator=(const WebDebugServer&) = delete;
|
WebDebugServer& operator=(const WebDebugServer&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint16_t port_;
|
uint16_t port_;
|
||||||
SnapshotFn snapshot_fn_;
|
SnapshotFn snapshot_fn_;
|
||||||
|
const char* html_;
|
||||||
httplib::Server svr_;
|
httplib::Server svr_;
|
||||||
std::thread thread_;
|
std::thread thread_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace kpn::web_debug
|
} // namespace kpn::web_debug
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user