KPN/search/search_index.json

1 line
30 KiB
JSON

{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"KPN++","text":"<p>A C++20 Kahn Process Network library. Each node wraps a plain function and runs concurrently, communicating with downstream nodes via bounded FIFO channels. Includes Python bindings via nanobind.</p>"},{"location":"#why-kpn","title":"Why KPN++?","text":"<ul> <li>Zero boilerplate \u2014 wrap any callable as a node; types flow automatically from the function signature</li> <li>Bounded channels \u2014 backpressure is structural, not bolted on</li> <li>Observable \u2014 per-node and network-level callbacks for overflow and stop events; diagnostics snapshots; optional web UI</li> <li>Composable \u2014 <code>Network</code> for runtime wiring, <code>StaticNetwork</code> for compile-time topology with zero overhead</li> </ul>"},{"location":"#quick-example","title":"Quick example","text":"<pre><code>#include &lt;kpn/kpn.hpp&gt;\nusing namespace kpn;\n\nstatic int produce() { return 42; }\nstatic int double_it(int x) { return x * 2; }\nstatic void print_it(int x) { std::cout &lt;&lt; \"result: \" &lt;&lt; x &lt;&lt; '\\n'; }\n\nint main() {\n Network net;\n net.add(\"src\", src)\n .add(\"dbl\", dbl)\n .add(\"sink\", sink)\n .connect(\"src\", src.output&lt;0&gt;(), \"dbl\", dbl.input&lt;0&gt;())\n .connect(\"dbl\", dbl.output&lt;0&gt;(), \"sink\", sink.input&lt;0&gt;())\n .build();\n\n net.start();\n std::this_thread::sleep_for(std::chrono::milliseconds(100));\n net.stop();\n}\n</code></pre>"},{"location":"#install-build","title":"Install &amp; build","text":"<pre><code>cmake -B build\ncmake --build build --parallel\nctest --test-dir build # unit tests + example smoke tests\n</code></pre> <p>See Getting Started for full build options.</p>"},{"location":"channels/","title":"Channels","text":"<p>A <code>Channel&lt;T&gt;</code> is a lock-free SPSC (single-producer, single-consumer) ring buffer with atomic wait/notify.</p>"},{"location":"channels/#semantics","title":"Semantics","text":"<ul> <li>Bounded: fixed capacity set at construction. Default is 5 items.</li> <li>Backpressure: when full, <code>push()</code> throws <code>ChannelOverflowError</code> immediately \u2014 no blocking, no spin.</li> <li>Blocking consumer: <code>pop()</code> blocks until an item is available or the channel is disabled.</li> <li>Disable: <code>channel.disable()</code> stops accepting pushes and unblocks any waiting <code>pop()</code> with <code>ChannelClosedError</code>.</li> </ul>"},{"location":"channels/#storage-policy","title":"Storage policy","text":"<p>Small trivially-copyable types (\u2264 8 bytes) are stored by value. Larger types are heap-allocated and passed via <code>shared_ptr&lt;const T&gt;</code> \u2014 one allocation per push, zero-copy fan-out:</p> <pre><code>// Override: store Tag by value despite being a struct\n// (it's trivially copyable and small \u2014 this just makes the policy explicit)\ntemplate&lt;&gt;\nstruct kpn::channel_storage_policy&lt;Tag&gt; {\n static constexpr bool by_value = true;\n};\n</code></pre> <p>Specialize <code>kpn::ChannelDataSize&lt;T&gt;</code> for accurate bandwidth reporting on heap-owning types:</p> <pre><code>template&lt;&gt;\nstruct kpn::ChannelDataSize&lt;cv::Mat&gt; {\n static std::size_t bytes(const cv::Mat&amp; m) { return m.total() * m.elemSize(); }\n};\n</code></pre>"},{"location":"channels/#named-ports","title":"Named ports","text":"<p><code>in&lt;\"name\"&gt;</code> and <code>out&lt;\"name\"&gt;</code> tag nodes for readable wiring:</p> <pre><code> // tokenise: no inputs, one named output \"words\"\n auto tok = make_node&lt;tokenise&gt;(out&lt;\"words\"&gt;{}, 4);\n\n // count_words: named input \"words\", named outputs \"count\" and \"words\"\n auto cnt = make_node&lt;count_words&gt;(in&lt;\"words\"&gt;{}, out&lt;\"count\", \"words\"&gt;{}, 4);\n\n // report: two named inputs\n auto snk = make_node&lt;report&gt;(in&lt;\"count\", \"words\"&gt;{}, 4);\n</code></pre> <p>Named ports are checked at compile time \u2014 a typo in a port name is a compile error.</p>"},{"location":"channels/#capacity-tuning","title":"Capacity tuning","text":"<p>Set capacity per node at construction:</p> <pre><code>auto node = make_node&lt;my_func&gt;(/*capacity=*/20);\n</code></pre> <p>Capacity is rounded up internally to the next power of two. Monitor fill levels via diagnostics to tune for your workload \u2014 a too-small capacity causes overflows; a too-large one wastes memory and hides producer/consumer speed mismatches.</p>"},{"location":"channels/#spin-count","title":"Spin count","text":"<p><code>Channel</code> spins for up to ~4 \u00b5s (200 <code>pause</code> hints at ~20 ns each on x86) before sleeping on a futex. Set to 0 for power-constrained or predominantly-idle pipelines:</p> <pre><code>Channel&lt;int&gt; ch(/*capacity=*/5, /*spin_count=*/0);\n</code></pre>"},{"location":"error-handling/","title":"Error Handling &amp; Events","text":"<p>KPN++ provides three complementary layers for observing and reacting to failures.</p>"},{"location":"error-handling/#1-per-node-error-handler","title":"1. Per-node error handler","text":"<p>Called when a node's function throws an unhandled exception. Return <code>true</code> to skip the failed invocation and keep running; <code>false</code> to stop the node.</p> <pre><code> // Return true \u2192 skip this invocation, keep the node running.\n // Return false \u2192 stop the node (downstream drains then also stops).\n proc.set_error_handler([](std::string_view name, std::exception_ptr ep) {\n try { std::rethrow_exception(ep); }\n catch (const std::exception&amp; e) {\n std::cerr &lt;&lt; \"[\" &lt;&lt; name &lt;&lt; \"] skipping item \u2014 \" &lt;&lt; e.what() &lt;&lt; '\\n';\n }\n return true;\n });\n</code></pre> <p>When a node stops (either from <code>false</code> return or no handler installed), it:</p> <ol> <li>Disables its input channels \u2014 upstream stops pushing into dead queues.</li> <li>Disables its output channels \u2014 downstream nodes receive <code>ChannelClosedError</code> on their next pop, propagating the shutdown naturally through the graph.</li> </ol>"},{"location":"error-handling/#2-per-node-overflow-callback","title":"2. Per-node overflow callback","text":"<p>Fired with a timestamp each time an output push is dropped because the channel is full. The node name is known at registration so it is not included \u2014 keeping the callback zero-overhead when unused.</p> <pre><code> // Per-node overflow callback \u2014 no node name needed, known at registration.\n std::atomic&lt;int&gt; overflow_count{0};\n src.set_overflow_callback([&amp;](steady_clock::time_point ts) {\n auto ms = duration_cast&lt;milliseconds&gt;(ts.time_since_epoch()).count();\n std::cerr &lt;&lt; \"[overflow] fast_source at t=\" &lt;&lt; ms &lt;&lt; \"ms\\n\";\n overflow_count.fetch_add(1);\n });\n</code></pre> <p>Note</p> <p>The callback is purely informational \u2014 the node always continues after an overflow. To stop the node on overflow, call <code>node.stop()</code> from inside the callback.</p> <p>A matching <code>set_closed_callback()</code> fires (also with just a timestamp) when the node stops due to a closed upstream channel:</p> <pre><code>node.set_closed_callback([](std::chrono::steady_clock::time_point ts) {\n std::cerr &lt;&lt; \"node stopped at t=\" &lt;&lt; ts.time_since_epoch().count() &lt;&lt; '\\n';\n});\n</code></pre> <p>Each node holds two callback slots per event type \u2014 one user-set (registered above) and one injected by the network (see below). Both fire independently.</p>"},{"location":"error-handling/#3-network-level-event-handler","title":"3. Network-level event handler","text":"<p>One callback for the whole network. Receives the node name (captured in a closure by the network at <code>build()</code> / <code>start()</code>), a <code>NodeEvent</code>, and a timestamp:</p> <pre><code> // Network-level aggregate handler \u2014 covers every node, includes node name.\n net.set_event_handler([](std::string_view name, NodeEvent ev,\n steady_clock::time_point ts) {\n auto ms = duration_cast&lt;milliseconds&gt;(ts.time_since_epoch()).count();\n std::string_view kind = (ev == NodeEvent::Overflow) ? \"overflow\" : \"closed\";\n std::cerr &lt;&lt; \"[net:\" &lt;&lt; kind &lt;&lt; \"] node=\" &lt;&lt; name &lt;&lt; \" t=\" &lt;&lt; ms &lt;&lt; \"ms\\n\";\n });\n</code></pre> <p><code>NodeEvent</code> values:</p> Value Meaning <code>NodeEvent::Overflow</code> An output push was dropped (channel full) <code>NodeEvent::Closed</code> The node stopped (crash or upstream close cascade) <p>The network handler and any per-node callbacks are independent \u2014 both fire when set.</p>"},{"location":"error-handling/#complete-example","title":"Complete example","text":"<p><code>examples/16_event_callbacks/main.cpp</code> shows a fast producer overflowing a slow consumer, with both a per-node overflow callback and a network-level event handler active simultaneously.</p> <p>Node functions:</p> <pre><code>static std::atomic&lt;int&gt; g_seq{0};\n\nstatic int fast_source() {\n std::this_thread::sleep_for(milliseconds(2)); // ~500/s\n return g_seq.fetch_add(1);\n}\n\nstatic void slow_sink(int x) {\n std::this_thread::sleep_for(milliseconds(50)); // ~20/s\n std::cout &lt;&lt; \" consumed: \" &lt;&lt; x &lt;&lt; '\\n';\n}\n</code></pre> <p>Per-node overflow callback:</p> <pre><code> // Per-node overflow callback \u2014 no node name needed, known at registration.\n std::atomic&lt;int&gt; overflow_count{0};\n src.set_overflow_callback([&amp;](steady_clock::time_point ts) {\n auto ms = duration_cast&lt;milliseconds&gt;(ts.time_since_epoch()).count();\n std::cerr &lt;&lt; \"[overflow] fast_source at t=\" &lt;&lt; ms &lt;&lt; \"ms\\n\";\n overflow_count.fetch_add(1);\n });\n</code></pre> <p>Network-level event handler:</p> <pre><code> // Network-level aggregate handler \u2014 covers every node, includes node name.\n net.set_event_handler([](std::string_view name, NodeEvent ev,\n steady_clock::time_point ts) {\n auto ms = duration_cast&lt;milliseconds&gt;(ts.time_since_epoch()).count();\n std::string_view kind = (ev == NodeEvent::Overflow) ? \"overflow\" : \"closed\";\n std::cerr &lt;&lt; \"[net:\" &lt;&lt; kind &lt;&lt; \"] node=\" &lt;&lt; name &lt;&lt; \" t=\" &lt;&lt; ms &lt;&lt; \"ms\\n\";\n });\n</code></pre>"},{"location":"examples/","title":"Examples","text":"<p>All C++ examples are built by default and registered as CTest smoke tests. Run them all with:</p> <pre><code>ctest --test-dir build -L examples\n</code></pre>"},{"location":"examples/#index","title":"Index","text":"Example What it shows <code>01_hello_pipeline</code> Linear pipeline, index-based port wiring <code>02_named_ports</code> <code>in&lt;&gt;</code>/<code>out&lt;&gt;</code> name tags, named port access <code>03_multi_output</code> Tuple-returning node, per-element routing <code>04_storage_policy</code> <code>channel_storage_policy</code> specialisation <code>05_error_handling</code> Diagnostics handler, overflow channel stats <code>06_watchdog</code> Watchdog interval, stall detection <code>10_static_hello_pipeline</code> <code>StaticNetwork</code> + <code>make_network()</code> <code>11_static_fanout</code> <code>StaticNetwork</code> with <code>FanoutNode</code> <code>15_node_error_handler</code> <code>set_error_handler()</code> \u2014 skip or stop on exception <code>16_event_callbacks</code> <code>set_overflow_callback()</code>, <code>set_event_handler()</code>"},{"location":"examples/#opencv-examples-optional","title":"OpenCV examples (optional)","text":"<p>Built only when OpenCV \u2265 4 is found:</p> Example What it shows <code>09_opencv_cellshade</code> Real-time cell-shading on webcam; <code>MainThreadNode</code> for display <code>12_static_cellshade</code> Same pipeline as a <code>StaticNetwork</code> <code>13_debug_cellshade</code> Web debug UI overlay on the cell-shading pipeline <p>Run the cell-shading example:</p> <pre><code>./build/examples/09_opencv_cellshade\n# Press 'q' or close the window to stop.\n# Falls back to an animated synthetic pattern if no webcam is found.\n</code></pre>"},{"location":"fanout/","title":"Fan-out &amp; Routing","text":""},{"location":"fanout/#fanoutnode","title":"FanoutNode","text":"<p>Reads one item and pushes a copy to each of N output channels. All downstream nodes receive every item.</p> <pre><code>auto fan = make_fanout&lt;Image, 2&gt;(/*capacity=*/8);\n\nnet.connect(\"src\", src.output&lt;0&gt;(), \"fan\", fan.input&lt;0&gt;())\n .connect(\"fan\", fan.output&lt;0&gt;(), \"nodeA\", nodeA.input&lt;0&gt;())\n .connect(\"fan\", fan.output&lt;1&gt;(), \"nodeB\", nodeB.input&lt;0&gt;());\n</code></pre> <p>If one downstream channel overflows, that output drops the item independently \u2014 the other outputs are unaffected.</p> <p>See <code>examples/11_static_fanout</code>.</p>"},{"location":"fanout/#routernode","title":"RouterNode","text":"<p>Reads one item and pushes it to exactly one of N outputs, chosen by a selector function:</p> <pre><code>auto router = make_router&lt;Frame, 3&gt;(\n [](const Frame&amp; f) -&gt; std::size_t { return f.stream_id % 3; });\n\nnet.connect(\"src\", src.output&lt;0&gt;(), \"router\", router.input&lt;0&gt;())\n .connect(\"router\", router.output&lt;0&gt;(), \"nodeA\", nodeA.input&lt;0&gt;())\n .connect(\"router\", router.output&lt;1&gt;(), \"nodeB\", nodeB.input&lt;0&gt;())\n .connect(\"router\", router.output&lt;2&gt;(), \"nodeC\", nodeC.input&lt;0&gt;());\n</code></pre> <p>If the selector returns <code>&gt;= N</code> the item is silently dropped.</p>"},{"location":"fanout/#filternode","title":"FilterNode","text":"<p>Reads one item and passes it downstream only when a predicate returns <code>true</code>:</p> <pre><code>auto filt = make_filter&lt;Frame&gt;([](const Frame&amp; f) { return f.valid; });\n\nnet.connect(\"src\", src.output&lt;0&gt;(), \"filt\", filt.input&lt;0&gt;())\n .connect(\"filt\", filt.output&lt;0&gt;(), \"dst\", dst.input&lt;0&gt;());\n</code></pre>"},{"location":"getting-started/","title":"Getting Started","text":""},{"location":"getting-started/#requirements","title":"Requirements","text":"Dependency Version Notes CMake \u2265 3.21 C++ compiler GCC \u2265 11, Clang \u2265 13 C++20 required nanobind \u2265 2.1 auto-fetched; Python \u2265 3.8 Catch2 v3 auto-fetched for tests OpenCV \u2265 4 optional; only for examples 09/12/13"},{"location":"getting-started/#build","title":"Build","text":"<pre><code>cmake -B build # core + tests + C++ examples\ncmake --build build --parallel\nctest --test-dir build # run all tests including example smoke tests\n</code></pre> <p>Enable Python bindings:</p> <pre><code>cmake -B build -DKPN_BUILD_PYTHON=ON\ncmake --build build --parallel\n</code></pre> <p>Skip examples:</p> <pre><code>cmake -B build -DKPN_BUILD_EXAMPLES=OFF\n</code></pre>"},{"location":"getting-started/#your-first-pipeline","title":"Your first pipeline","text":"<p>Three functions \u2014 source, transform, sink \u2014 wired into a <code>Network</code>:</p> <pre><code>static int produce() { return 42; }\nstatic int double_it(int x) { return x * 2; }\nstatic void print_it(int x) { std::cout &lt;&lt; \"result: \" &lt;&lt; x &lt;&lt; '\\n'; }\n</code></pre> <p>Create nodes, connect them, build and run:</p> <pre><code> Network net;\n net.add(\"src\", src)\n .add(\"dbl\", dbl)\n .add(\"sink\", sink)\n .connect(\"src\", src.output&lt;0&gt;(), \"dbl\", dbl.input&lt;0&gt;())\n .connect(\"dbl\", dbl.output&lt;0&gt;(), \"sink\", sink.input&lt;0&gt;())\n .build();\n\n net.start();\n std::this_thread::sleep_for(std::chrono::milliseconds(100));\n net.stop();\n</code></pre> <p>That's it. Types are inferred from function signatures. The channel between <code>src</code> and <code>dbl</code> carries <code>int</code>; the channel between <code>dbl</code> and <code>prn</code> also carries <code>int</code>. A type mismatch is a compile error.</p>"},{"location":"getting-started/#named-ports","title":"Named ports","text":"<p>For nodes with multiple inputs or outputs, name the ports for clarity:</p> <pre><code> // tokenise: no inputs, one named output \"words\"\n auto tok = make_node&lt;tokenise&gt;(out&lt;\"words\"&gt;{}, 4);\n\n // count_words: named input \"words\", named outputs \"count\" and \"words\"\n auto cnt = make_node&lt;count_words&gt;(in&lt;\"words\"&gt;{}, out&lt;\"count\", \"words\"&gt;{}, 4);\n\n // report: two named inputs\n auto snk = make_node&lt;report&gt;(in&lt;\"count\", \"words\"&gt;{}, 4);\n</code></pre> <p>Wire by name instead of index:</p> <pre><code> Network net;\n net.add(\"tok\", tok)\n .add(\"cnt\", cnt)\n .add(\"snk\", snk)\n .connect(\"tok\", tok.template output&lt;\"words\"&gt;(), \"cnt\", cnt.template input&lt;\"words\"&gt;())\n .connect(\"cnt\", cnt.template output&lt;\"count\"&gt;(), \"snk\", snk.template input&lt;\"count\"&gt;())\n .connect(\"cnt\", cnt.template output&lt;\"words\"&gt;(), \"snk\", snk.template input&lt;\"words\"&gt;())\n .build();\n\n net.start();\n std::this_thread::sleep_for(std::chrono::milliseconds(500));\n net.stop();\n</code></pre>"},{"location":"getting-started/#multi-output-nodes","title":"Multi-output nodes","text":"<p>Return a <code>std::tuple</code> to fan out to multiple downstream nodes:</p> <pre><code>// Multi-output: returns (key, value) as a tuple \u2014 KPN++ routes each element\n// to its own output port automatically.\nstatic std::tuple&lt;std::string, std::string&gt; parse(std::string kv) {\n auto sep = kv.find('=');\n if (sep == std::string::npos) return {kv, \"\"};\n return {kv.substr(0, sep), kv.substr(sep + 1)};\n}\n</code></pre> <p>Wire each tuple element to its own downstream node:</p> <pre><code> auto gen = make_node&lt;generate&gt;(out&lt;\"kv\"&gt;{}, 4);\n auto par = make_node&lt;parse&gt; (in&lt;\"kv\"&gt;{}, out&lt;\"key\", \"value\"&gt;{}, 4);\n auto keys = make_node&lt;print_key&gt; (in&lt;\"key\"&gt;{}, 4);\n auto vals = make_node&lt;print_value&gt;(in&lt;\"value\"&gt;{}, 4);\n\n Network net;\n net.add(\"gen\", gen)\n .add(\"par\", par)\n .add(\"keys\", keys)\n .add(\"vals\", vals)\n .connect(\"gen\", gen.template output&lt;\"kv\"&gt;(), \"par\", par.template input&lt;\"kv\"&gt;())\n .connect(\"par\", par.template output&lt;\"key\"&gt;(), \"keys\", keys.template input&lt;\"key\"&gt;())\n .connect(\"par\", par.template output&lt;\"value\"&gt;(), \"vals\", vals.template input&lt;\"value\"&gt;())\n .build();\n\n net.start();\n std::this_thread::sleep_for(std::chrono::milliseconds(600));\n net.stop();\n</code></pre>"},{"location":"network/","title":"Networks","text":"<p>A <code>Network</code> wires nodes together at runtime using a builder chain.</p>"},{"location":"network/#building-a-network","title":"Building a network","text":"<pre><code> Network net;\n net.add(\"src\", src)\n .add(\"dbl\", dbl)\n .add(\"sink\", sink)\n .connect(\"src\", src.output&lt;0&gt;(), \"dbl\", dbl.input&lt;0&gt;())\n .connect(\"dbl\", dbl.output&lt;0&gt;(), \"sink\", sink.input&lt;0&gt;())\n .build();\n\n net.start();\n std::this_thread::sleep_for(std::chrono::milliseconds(100));\n net.stop();\n</code></pre> <p>The builder chain:</p> Method Purpose <code>.add(name, node)</code> Register a node; assigns its name <code>.connect(src, port, dst, port)</code> Wire one output port to one input port <code>.build()</code> Compute topological order; inject network callbacks <code>.start()</code> Start nodes in topological order <code>.stop()</code> Stop all nodes immediately <code>.shutdown()</code> Graceful drain: stop sources first, wait for channels to empty, then stop downstream"},{"location":"network/#port-access","title":"Port access","text":"<p>Ports are accessed by index or by name:</p> <pre><code>// By index\nnet.connect(\"src\", src.output&lt;0&gt;(), \"dst\", dst.input&lt;0&gt;());\n\n// By name (requires named ports)\n Network net;\n net.add(\"tok\", tok)\n .add(\"cnt\", cnt)\n .add(\"snk\", snk)\n .connect(\"tok\", tok.template output&lt;\"words\"&gt;(), \"cnt\", cnt.template input&lt;\"words\"&gt;())\n .connect(\"cnt\", cnt.template output&lt;\"count\"&gt;(), \"snk\", snk.template input&lt;\"count\"&gt;())\n .connect(\"cnt\", cnt.template output&lt;\"words\"&gt;(), \"snk\", snk.template input&lt;\"words\"&gt;())\n .build();\n\n net.start();\n std::this_thread::sleep_for(std::chrono::milliseconds(500));\n net.stop();\n</code></pre>"},{"location":"network/#diagnostics","title":"Diagnostics","text":"<p>Install a diagnostics handler to receive periodic snapshots of every node and channel:</p> <pre><code> // Custom diagnostics handler \u2014 fires on the watchdog interval.\n // Print a concise one-liner rather than the full table.\n net.set_diagnostics_handler([](const std::vector&lt;NodeSnapshot&gt;&amp; nodes,\n const std::vector&lt;ChannelSnapshot&gt;&amp; channels) {\n std::cout &lt;&lt; \"[diag] \";\n for (auto&amp; n : nodes)\n std::cout &lt;&lt; n.name &lt;&lt; \"=\" &lt;&lt; n.throughput_fps &lt;&lt; \"fps \";\n for (auto&amp; c : channels)\n std::cout &lt;&lt; \"channel fill=\" &lt;&lt; static_cast&lt;int&gt;(c.fill_pct()) &lt;&lt; \"% \"\n &lt;&lt; \"overflows=\" &lt;&lt; c.overflows;\n std::cout &lt;&lt; '\\n';\n });\n</code></pre> <p>Or print a full report at any time:</p> <pre><code>net.print_diagnostics(); // writes to stderr by default\nnet.print_diagnostics(std::cout);\n</code></pre>"},{"location":"network/#network-level-event-handler","title":"Network-level event handler","text":"<p>Observe overflow and node-stop events across the entire network in one place:</p> <pre><code> // Network-level aggregate handler \u2014 covers every node, includes node name.\n net.set_event_handler([](std::string_view name, NodeEvent ev,\n steady_clock::time_point ts) {\n auto ms = duration_cast&lt;milliseconds&gt;(ts.time_since_epoch()).count();\n std::string_view kind = (ev == NodeEvent::Overflow) ? \"overflow\" : \"closed\";\n std::cerr &lt;&lt; \"[net:\" &lt;&lt; kind &lt;&lt; \"] node=\" &lt;&lt; name &lt;&lt; \" t=\" &lt;&lt; ms &lt;&lt; \"ms\\n\";\n });\n</code></pre> <p><code>NodeEvent</code> is either <code>NodeEvent::Overflow</code> (item dropped on full channel) or <code>NodeEvent::Closed</code> (node stopped due to crash or closed upstream channel). See Error Handling &amp; Events.</p>"},{"location":"network/#shutdown","title":"Shutdown","text":"<p><code>net.stop()</code> halts immediately \u2014 all nodes stop in reverse topological order.</p> <p><code>net.shutdown()</code> drains gracefully: source nodes stop first; their output channels are polled until empty; then the next layer stops, and so on. This ensures no items are lost if downstream nodes are still consuming.</p>"},{"location":"network/#staticnetwork","title":"StaticNetwork","text":"<p>For zero-overhead compile-time topology, see Static Networks.</p>"},{"location":"nodes/","title":"Nodes","text":"<p>A node wraps any callable. Its input types are inferred from the function's parameter list; its output types from the return type.</p>"},{"location":"nodes/#node-types","title":"Node types","text":"Type Thread model Use case <code>Node&lt;Func&gt;</code> Dedicated thread per node Default \u2014 simplest, most isolated <code>PoolNode&lt;Func&gt;</code> Shared <code>ThreadPool</code> Many nodes, resource-bounded execution <code>InterruptNode&lt;Func&gt;</code> Event-driven, no thread Camera frame ready, timer tick, socket <code>FanoutNode&lt;T, N&gt;</code> Dedicated thread Broadcast one item to N outputs <code>RouterNode&lt;T, N&gt;</code> Dedicated thread Route one item to one of N outputs <code>FilterNode&lt;T&gt;</code> Dedicated thread Pass items matching a predicate"},{"location":"nodes/#creating-nodes","title":"Creating nodes","text":"<p>All node types are created via factory functions that infer types from the callable:</p> <pre><code>// Free function \u2014 simplest case\nauto node = make_node&lt;my_func&gt;();\n\n// Stateful functor (operator() is the function)\nMyProcessor proc;\nauto node = make_node(proc);\n\n// Pool node \u2014 shares a ThreadPool with other nodes\nauto pool = std::make_shared&lt;ThreadPool&gt;(4);\nauto node = make_pool_node&lt;my_func&gt;(pool);\n\n// Interrupt node \u2014 triggered externally\nauto sched = std::make_shared&lt;ThreadPool&gt;(2);\nauto node = make_interrupt_node&lt;produce_frame&gt;(sched, out&lt;\"frame\"&gt;{});\ncamera_sdk.on_frame_ready(node.get_trigger());\n</code></pre>"},{"location":"nodes/#channel-capacity","title":"Channel capacity","text":"<p>Each node's input FIFO has a configurable capacity (default 5):</p> <pre><code>auto node = make_node&lt;my_func&gt;(/*capacity=*/20);\nauto node = make_pool_node&lt;my_func&gt;(pool, /*capacity=*/20);\n</code></pre> <p>When an upstream push would exceed capacity, <code>ChannelOverflowError</code> is thrown and the item is dropped. See Error Handling &amp; Events to observe and react to this.</p>"},{"location":"nodes/#source-nodes","title":"Source nodes","text":"<p>A node with no inputs is a source. It self-submits immediately on <code>start()</code> and re-submits after each execution:</p> <pre><code>static int produce() {\n std::this_thread::sleep_for(std::chrono::milliseconds(10));\n return ++counter;\n}\nauto src = make_node&lt;produce&gt;();\n</code></pre> <p>Tip</p> <p>Source nodes must sleep or yield to avoid overflowing their output channel. The channel capacity provides the only bound.</p>"},{"location":"nodes/#sink-nodes","title":"Sink nodes","text":"<p>A node with a <code>void</code> return is a sink \u2014 it consumes items without producing output:</p> <pre><code>static void print_it(int x) { std::cout &lt;&lt; x &lt;&lt; '\\n'; }\nauto snk = make_node&lt;print_it&gt;();\n</code></pre>"},{"location":"nodes/#error-handler","title":"Error handler","text":"<p>When a node's function throws an unhandled exception, the default behaviour is to stop the node (disabling its channels so the shutdown cascades downstream). Install a handler to override:</p> <pre><code> // Return true \u2192 skip this invocation, keep the node running.\n // Return false \u2192 stop the node (downstream drains then also stops).\n proc.set_error_handler([](std::string_view name, std::exception_ptr ep) {\n try { std::rethrow_exception(ep); }\n catch (const std::exception&amp; e) {\n std::cerr &lt;&lt; \"[\" &lt;&lt; name &lt;&lt; \"] skipping item \u2014 \" &lt;&lt; e.what() &lt;&lt; '\\n';\n }\n return true;\n });\n</code></pre> <p>See Error Handling &amp; Events for the full picture.</p>"},{"location":"shared-resource/","title":"Shared Resources","text":"<p><code>SharedResource&lt;T&gt;</code> arbitrates exclusive access to a resource (ONNX session, CUDA stream, serial port) across multiple nodes using a priority-based waiter queue with starvation prevention.</p>"},{"location":"shared-resource/#usage","title":"Usage","text":"<pre><code>#include &lt;kpn/shared_resource.hpp&gt;\nusing namespace kpn;\n\nSharedResource&lt;OnnxSession&gt; model(session_args...);\n\nstatic cv::Mat run_inference(cv::Mat frame) {\n // Acquires the model; releases automatically on scope exit.\n auto guard = model.acquire_balanced(in_channel, out_channel);\n return guard-&gt;Run(frame);\n}\n</code></pre>"},{"location":"shared-resource/#acquire-modes","title":"Acquire modes","text":"Method Priority <code>acquire()</code> Equal (fair FIFO) <code>acquire(fn)</code> Custom \u2014 <code>fn()</code> returns <code>float</code> in <code>[0, 1]</code> <code>acquire_balanced(in_ch, out_ch)</code> <code>input_fill \u00d7 output_headroom</code> \u2014 highest urgency wins <p><code>acquire_balanced</code> favours nodes with full input queues and empty output queues \u2014 the node that has the most work to do and nowhere to stall wins the resource next.</p>"},{"location":"shared-resource/#starvation-prevention","title":"Starvation prevention","text":"<p>Each waiter's effective score grows with elapsed wait time (<code>0.05</code> per second by default), ensuring a low-priority node eventually gets served regardless of how frequently higher-priority nodes compete.</p>"},{"location":"shared-resource/#diagnostics","title":"Diagnostics","text":"<p>Register with the network for snapshot reporting:</p> <pre><code>net.register_resource(\"model\", &amp;model);\n</code></pre> <p>The diagnostics table then shows acquisition count, mean wait time, and current waiter count.</p>"},{"location":"static-network/","title":"Static Networks","text":"<p><code>StaticNetwork</code> encodes the entire topology at compile time using a <code>make_network()</code> builder. Nodes and channel types are verified statically with zero runtime overhead.</p>"},{"location":"static-network/#usage","title":"Usage","text":"<pre><code>#include &lt;kpn/kpn.hpp&gt;\nusing namespace kpn;\n\nstatic int produce() { return 42; }\nstatic int double_it(int x) { return x * 2; }\nstatic void print_it(int x) { std::cout &lt;&lt; x &lt;&lt; '\\n'; }\n\nint main() {\n auto src = make_node&lt;produce&gt; ();\n auto dbl = make_node&lt;double_it&gt;();\n auto prn = make_node&lt;print_it&gt; ();\n\n auto net = make_network(\n edge(src, src.output&lt;0&gt;(), dbl, dbl.input&lt;0&gt;()),\n edge(dbl, dbl.output&lt;0&gt;(), prn, prn.input&lt;0&gt;())\n );\n\n net.set_event_handler([](std::string_view name, NodeEvent ev, auto ts) {\n // same API as Network\n });\n\n net.start();\n std::this_thread::sleep_for(std::chrono::milliseconds(100));\n net.stop();\n}\n</code></pre> <p>See <code>examples/10_static_hello_pipeline</code> and <code>examples/11_static_fanout</code>.</p>"},{"location":"static-network/#when-to-use","title":"When to use","text":"<code>Network</code> <code>StaticNetwork</code> Topology known at Runtime Compile time Type checking Runtime (<code>dynamic_cast</code>) Compile time Overhead Minimal Zero Flexibility Add nodes dynamically Fixed at compile time <p>For most applications <code>Network</code> is sufficient. Use <code>StaticNetwork</code> when you need the absolute minimum overhead or want compile-time topology verification.</p>"}]}