@@ -1,3 +1,80 @@
|
||||
// Example 02 — Named Ports
|
||||
//
|
||||
// A three-stage text pipeline where port names carry semantic meaning:
|
||||
//
|
||||
// [tokenise] --"words"--> [count_words] --"count"--> [report]
|
||||
// --"words"--> [report]
|
||||
//
|
||||
// Named ports let the connect() call read like documentation:
|
||||
// connect("tok", tok.output<"words">(), "cnt", cnt.input<"words">())
|
||||
// A typo in the name is a compile-time error, not a runtime surprise.
|
||||
|
||||
#include <kpn/kpn.hpp>
|
||||
// TODO: implement example
|
||||
int main() { return 0; }
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
// ── Node functions ────────────────────────────────────────────────────────────
|
||||
|
||||
static int sentence_index = 0;
|
||||
|
||||
static std::vector<std::string> tokenise() {
|
||||
static const char* sentences[] = {
|
||||
"the quick brown fox jumps over the lazy dog",
|
||||
"kahn process networks are a model of concurrent computation",
|
||||
"each node runs in its own thread communicating via channels",
|
||||
"named ports catch wiring mistakes at compile time",
|
||||
};
|
||||
std::istringstream ss(sentences[sentence_index++ % 4]);
|
||||
std::vector<std::string> words;
|
||||
std::string w;
|
||||
while (ss >> w) words.push_back(w);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
return words;
|
||||
}
|
||||
|
||||
// Returns (word_count, original_words) — two outputs via tuple
|
||||
static std::tuple<int, std::vector<std::string>>
|
||||
count_words(std::vector<std::string> words) {
|
||||
return {static_cast<int>(words.size()), std::move(words)};
|
||||
}
|
||||
|
||||
static void report(int count, std::vector<std::string> words) {
|
||||
std::cout << "[" << count << " words] ";
|
||||
for (auto& w : words) std::cout << w << ' ';
|
||||
std::cout << '\n';
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
using namespace kpn;
|
||||
|
||||
// tokenise: no inputs, one named output "words"
|
||||
auto tok = make_node<tokenise>(out<"words">{}, 4);
|
||||
|
||||
// count_words: named input "words", named outputs "count" and "words"
|
||||
auto cnt = make_node<count_words>(in<"words">{}, out<"count", "words">{}, 4);
|
||||
|
||||
// report: two named inputs — note the function takes (int, vector<string>)
|
||||
// so we need two separate input ports wired independently
|
||||
// For a two-input sink we wire each output of cnt to a different input of report
|
||||
auto snk = make_node<report>(in<"count", "words">{}, 4);
|
||||
|
||||
Network net;
|
||||
net.add("tok", tok)
|
||||
.add("cnt", cnt)
|
||||
.add("snk", snk)
|
||||
.connect("tok", tok.template output<"words">(), "cnt", cnt.template input<"words">())
|
||||
.connect("cnt", cnt.template output<"count">(), "snk", snk.template input<"count">())
|
||||
.connect("cnt", cnt.template output<"words">(), "snk", snk.template input<"words">())
|
||||
.build();
|
||||
|
||||
net.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
net.stop();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,75 @@
|
||||
// Example 03 — Multi-Output (Fan-Out)
|
||||
//
|
||||
// A single "parse" node reads "KEY=VALUE" strings and fans out to two
|
||||
// independent downstream sinks — one for keys, one for values.
|
||||
//
|
||||
// +--> [print_key]
|
||||
// [generate] --string--> [parse]
|
||||
// +--> [print_value]
|
||||
//
|
||||
// The parser returns std::tuple<std::string, std::string>.
|
||||
// Network::connect() routes each element of the tuple to a different node.
|
||||
|
||||
#include <kpn/kpn.hpp>
|
||||
// TODO: implement example
|
||||
int main() { return 0; }
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
|
||||
// ── Node functions ────────────────────────────────────────────────────────────
|
||||
|
||||
static int gen_index = 0;
|
||||
|
||||
static std::string generate() {
|
||||
static const char* pairs[] = {
|
||||
"host=localhost",
|
||||
"port=8080",
|
||||
"timeout=30s",
|
||||
"retries=3",
|
||||
"protocol=http2",
|
||||
};
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(60));
|
||||
return pairs[gen_index++ % 5];
|
||||
}
|
||||
|
||||
// Multi-output: returns (key, value) as a tuple — KPN++ routes each element
|
||||
// to its own output port automatically.
|
||||
static std::tuple<std::string, std::string> parse(std::string kv) {
|
||||
auto sep = kv.find('=');
|
||||
if (sep == std::string::npos) return {kv, ""};
|
||||
return {kv.substr(0, sep), kv.substr(sep + 1)};
|
||||
}
|
||||
|
||||
static void print_key(std::string key) {
|
||||
std::cout << "KEY → " << key << '\n';
|
||||
}
|
||||
|
||||
static void print_value(std::string value) {
|
||||
std::cout << "VALUE → " << value << '\n';
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
using namespace kpn;
|
||||
|
||||
auto gen = make_node<generate>(out<"kv">{}, 4);
|
||||
auto par = make_node<parse> (in<"kv">{}, out<"key", "value">{}, 4);
|
||||
auto keys = make_node<print_key> (in<"key">{}, 4);
|
||||
auto vals = make_node<print_value>(in<"value">{}, 4);
|
||||
|
||||
Network net;
|
||||
net.add("gen", gen)
|
||||
.add("par", par)
|
||||
.add("keys", keys)
|
||||
.add("vals", vals)
|
||||
.connect("gen", gen.template output<"kv">(), "par", par.template input<"kv">())
|
||||
.connect("par", par.template output<"key">(), "keys", keys.template input<"key">())
|
||||
.connect("par", par.template output<"value">(), "vals", vals.template input<"value">())
|
||||
.build();
|
||||
|
||||
net.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(600));
|
||||
net.stop();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,118 @@
|
||||
// Example 04 — Storage Policy
|
||||
//
|
||||
// Demonstrates how KPN++ chooses between by-value and shared_ptr storage
|
||||
// inside channels depending on the type.
|
||||
//
|
||||
// Small trivially-copyable types (int, double, etc.) are stored by value.
|
||||
// Large or non-trivially-copyable types are stored as shared_ptr<const T>
|
||||
// — zero copies even across multiple downstream consumers.
|
||||
//
|
||||
// This example also shows how to override the policy for a specific type
|
||||
// with a template specialisation.
|
||||
//
|
||||
// [produce_frame] --Frame--> [process_frame] --Frame--> [consume_frame]
|
||||
// [produce_small] --int----> [double_it] --int----> [print_int]
|
||||
|
||||
#include <kpn/kpn.hpp>
|
||||
// TODO: implement example
|
||||
int main() { return 0; }
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Large frame type — will be stored as shared_ptr<const Frame> by default
|
||||
struct Frame {
|
||||
std::array<uint8_t, 4096> pixels{};
|
||||
int id = 0;
|
||||
};
|
||||
|
||||
// A small struct we force to be stored by value via policy specialisation
|
||||
struct Tag {
|
||||
int value = 0;
|
||||
};
|
||||
|
||||
// Override: store Tag by value despite being a struct
|
||||
// (it's trivially copyable and small — this just makes the policy explicit)
|
||||
template<>
|
||||
struct kpn::channel_storage_policy<Tag> {
|
||||
static constexpr bool by_value = true;
|
||||
};
|
||||
|
||||
// ── Node functions ────────────────────────────────────────────────────────────
|
||||
|
||||
static int frame_id = 0;
|
||||
static int tag_id = 0;
|
||||
|
||||
static Frame produce_frame() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(40));
|
||||
Frame f;
|
||||
f.id = frame_id++;
|
||||
f.pixels.fill(static_cast<uint8_t>(f.id & 0xFF));
|
||||
return f;
|
||||
}
|
||||
|
||||
static Frame process_frame(Frame f) {
|
||||
// Simulate some work
|
||||
for (auto& p : f.pixels) p = static_cast<uint8_t>(255 - p);
|
||||
return f;
|
||||
}
|
||||
|
||||
static void consume_frame(Frame f) {
|
||||
std::cout << "[frame] id=" << f.id
|
||||
<< " first_px=" << static_cast<int>(f.pixels[0])
|
||||
<< " storage=shared_ptr (sizeof Frame = " << sizeof(Frame) << " B)\n";
|
||||
}
|
||||
|
||||
static Tag produce_tag() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(40));
|
||||
return {tag_id++};
|
||||
}
|
||||
|
||||
static Tag double_tag(Tag t) { return {t.value * 2}; }
|
||||
|
||||
static void print_tag(Tag t) {
|
||||
std::cout << "[tag] value=" << t.value
|
||||
<< " storage=by_value (sizeof Tag = " << sizeof(Tag) << " B)\n";
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
using namespace kpn;
|
||||
|
||||
// Verify storage decisions at compile time
|
||||
static_assert(!channel_storage_policy<Frame>::by_value,
|
||||
"Frame should use shared_ptr storage");
|
||||
static_assert(channel_storage_policy<Tag>::by_value,
|
||||
"Tag should use by_value storage (via specialisation)");
|
||||
static_assert(channel_storage_policy<int>::by_value,
|
||||
"int should use by_value storage");
|
||||
|
||||
auto prod_f = make_node<produce_frame> (out<"frame">{}, 4);
|
||||
auto proc_f = make_node<process_frame> (in<"frame">{}, out<"frame">{}, 4);
|
||||
auto cons_f = make_node<consume_frame> (in<"frame">{}, 4);
|
||||
|
||||
auto prod_t = make_node<produce_tag> (out<"tag">{}, 4);
|
||||
auto dbl_t = make_node<double_tag> (in<"tag">{}, out<"tag">{}, 4);
|
||||
auto print_t = make_node<print_tag> (in<"tag">{}, 4);
|
||||
|
||||
Network net;
|
||||
net.add("prod_f", prod_f)
|
||||
.add("proc_f", proc_f)
|
||||
.add("cons_f", cons_f)
|
||||
.add("prod_t", prod_t)
|
||||
.add("dbl_t", dbl_t)
|
||||
.add("print_t", print_t)
|
||||
.connect("prod_f", prod_f.template output<"frame">(), "proc_f", proc_f.template input<"frame">())
|
||||
.connect("proc_f", proc_f.template output<"frame">(), "cons_f", cons_f.template input<"frame">())
|
||||
.connect("prod_t", prod_t.template output<"tag">(), "dbl_t", dbl_t.template input<"tag">())
|
||||
.connect("dbl_t", dbl_t.template output<"tag">(), "print_t", print_t.template input<"tag">())
|
||||
.build();
|
||||
|
||||
net.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(400));
|
||||
net.stop();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,78 @@
|
||||
// Example 05 — Error Handling & Diagnostics
|
||||
//
|
||||
// Demonstrates observable failure modes and the diagnostics system:
|
||||
//
|
||||
// 1. ChannelOverflowError — a fast producer saturates a slow consumer.
|
||||
// When the channel is full, push() throws ChannelOverflowError.
|
||||
// The node's run_loop catches it and prints to stderr.
|
||||
// Channel statistics (overflows, peak fill) accumulate for the report.
|
||||
//
|
||||
// 2. Custom diagnostics handler — instead of the default periodic table,
|
||||
// install a handler that surfaces only the metrics you care about.
|
||||
//
|
||||
// 3. net.print_diagnostics() — print a full report at any time.
|
||||
//
|
||||
// Pipeline: [producer] --int--> [slow_consumer]
|
||||
|
||||
#include <kpn/kpn.hpp>
|
||||
// TODO: implement example
|
||||
int main() { return 0; }
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
// ── Node functions ────────────────────────────────────────────────────────────
|
||||
|
||||
// Produces at ~100/s — faster than the consumer can keep up (50 ms each)
|
||||
static int producer() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
static int n = 0;
|
||||
return ++n;
|
||||
}
|
||||
|
||||
// Slow consumer: 50 ms per item — will cause channel to fill and overflow
|
||||
static void slow_consumer(int x) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
std::cout << "[consumed] " << x << '\n';
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
using namespace kpn;
|
||||
|
||||
// Capacity=4: fills up quickly when producer outpaces consumer 5:1
|
||||
auto prod = make_node<producer> (out<"v">{}, /*capacity=*/4);
|
||||
auto cons = make_node<slow_consumer> (in<"v">{}, /*capacity=*/4);
|
||||
|
||||
Network net;
|
||||
|
||||
// Custom diagnostics handler — fires on the watchdog interval.
|
||||
// Print a concise one-liner rather than the full table.
|
||||
net.set_diagnostics_handler([](const std::vector<NodeSnapshot>& nodes,
|
||||
const std::vector<ChannelSnapshot>& channels) {
|
||||
std::cout << "[diag] ";
|
||||
for (auto& n : nodes)
|
||||
std::cout << n.name << "=" << n.throughput_fps << "fps ";
|
||||
for (auto& c : channels)
|
||||
std::cout << "channel fill=" << static_cast<int>(c.fill_pct()) << "% "
|
||||
<< "overflows=" << c.overflows;
|
||||
std::cout << '\n';
|
||||
});
|
||||
|
||||
net.set_watchdog_interval(std::chrono::milliseconds(200));
|
||||
|
||||
net.add("prod", prod)
|
||||
.add("cons", cons)
|
||||
.connect("prod", prod.template output<"v">(), "cons", cons.template input<"v">())
|
||||
.build();
|
||||
|
||||
std::cout << "producer: 10ms/item, consumer: 50ms/item, capacity=4\n"
|
||||
<< "Overflow messages appear on stderr; diagnostics on stdout.\n\n";
|
||||
|
||||
net.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||
net.stop();
|
||||
|
||||
// Full report after shutdown — overflow and drop counts are preserved
|
||||
std::cout << '\n';
|
||||
net.print_diagnostics();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,90 @@
|
||||
// Example 06 — Watchdog & Diagnostics (+ optional web debug UI)
|
||||
//
|
||||
// A four-stage pipeline with deliberately uneven node speeds:
|
||||
//
|
||||
// [source] --int--> [fast_filter] --int--> [slow_transform] --int--> [sink]
|
||||
//
|
||||
// The watchdog fires every second and prints a full diagnostics report:
|
||||
// - frames processed, throughput fps, exec time, blocked time per node
|
||||
// - channel fill %, peak fill %, pushes, overflows, bandwidth
|
||||
// - bottleneck hint (node with highest avg exec time)
|
||||
//
|
||||
// "slow_transform" sleeps 30 ms per item, making it the obvious bottleneck.
|
||||
// Watch the channel upstream of it saturate and the fps converge to ~33.
|
||||
//
|
||||
// Build with -DKPN_WEB_DEBUG=ON to also get a live D3 graph at localhost:9090.
|
||||
|
||||
#include <kpn/kpn.hpp>
|
||||
// TODO: implement example
|
||||
int main() { return 0; }
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
// ── Node functions ────────────────────────────────────────────────────────────
|
||||
|
||||
static int source() {
|
||||
// Produces at ~50 fps — faster than slow_transform (33 fps) so the
|
||||
// channel between filter and slow gradually fills, but not catastrophically
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
static int n = 0;
|
||||
return ++n;
|
||||
}
|
||||
|
||||
static int fast_filter(int x) {
|
||||
// Trivial work: ~0.1 ms
|
||||
return (x % 2 == 0) ? x : x + 1;
|
||||
}
|
||||
|
||||
static int slow_transform(int x) {
|
||||
// Simulates expensive processing (e.g. a neural net inference step)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
return x * x;
|
||||
}
|
||||
|
||||
static void sink(int x) {
|
||||
// Print every 10th result to avoid flooding the terminal
|
||||
if (x % 100 < 4)
|
||||
std::cout << "[sink] " << x << '\n';
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
using namespace kpn;
|
||||
|
||||
// Larger capacity buffers so the fast nodes don't immediately overflow
|
||||
auto src = make_node<source> (out<"v">{}, 16);
|
||||
auto filt = make_node<fast_filter> (in<"v">{}, out<"v">{}, 16);
|
||||
auto slow = make_node<slow_transform> (in<"v">{}, out<"v">{}, 8);
|
||||
auto snk = make_node<sink> (in<"v">{}, 8);
|
||||
|
||||
Network net;
|
||||
|
||||
// Watchdog fires every 1 second — prints the built-in diagnostics table
|
||||
// including the "Bottleneck hint" line
|
||||
net.set_watchdog_interval(std::chrono::milliseconds(1000));
|
||||
|
||||
net.add("source", src)
|
||||
.add("filter", filt)
|
||||
.add("slow", slow)
|
||||
.add("sink", snk)
|
||||
.connect("source", src.template output<"v">(), "filter", filt.template input<"v">())
|
||||
.connect("filter", filt.template output<"v">(), "slow", slow.template input<"v">())
|
||||
.connect("slow", slow.template output<"v">(), "sink", snk.template input<"v">())
|
||||
.build();
|
||||
|
||||
std::cout << "Running for 30 seconds — watch 'slow' become the bottleneck.\n"
|
||||
<< "Watchdog diagnostics print every 1 second.\n";
|
||||
#ifdef KPN_WEB_DEBUG
|
||||
net.set_web_debug_port(9090);
|
||||
std::cout << "Web debug UI: http://localhost:9090 (live graph, auto-updates every 500 ms)\n";
|
||||
#endif
|
||||
std::cout << '\n';
|
||||
|
||||
net.start();
|
||||
std::this_thread::sleep_for(std::chrono::seconds(30));
|
||||
net.stop();
|
||||
|
||||
std::cout << "\n=== Final diagnostics ===\n";
|
||||
net.print_diagnostics();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ kpn_example(03_multi_output)
|
||||
kpn_example(04_storage_policy)
|
||||
kpn_example(05_error_handling)
|
||||
kpn_example(06_watchdog)
|
||||
if(KPN_WEB_DEBUG)
|
||||
kpn_target_enable_web_debug(06_watchdog)
|
||||
endif()
|
||||
# 07 and 08 require the Python bindings — only add if built
|
||||
if(KPN_BUILD_PYTHON)
|
||||
# These are Python scripts, not compiled targets — installed alongside kpn_python
|
||||
|
||||
Reference in New Issue
Block a user