From 076f1194bfe13c3df6b0b13bbdb83fb2f9cd0a75 Mon Sep 17 00:00:00 2001 From: zacheryasc Date: Wed, 11 Feb 2026 15:23:26 +0000 Subject: [PATCH] feat: tui, agent interface, stats hook (#29) Runtime dashboard now features a TUI option and an interface for LLM tool use. Removed some bloat from stats collecting and replaced with a hook function to dump runtime stats into. --- Cargo.lock | 545 ++++++++++++- Cargo.toml | 4 + crates/runtime-dashboard/AGENTS.md | 175 +++++ crates/runtime-dashboard/Cargo.toml | 18 + crates/runtime-dashboard/README.md | 13 +- .../examples/bench_dashboard.rs | 25 +- .../examples/dashboard_demo.rs | 31 +- .../examples/investigate_demo.rs | 122 +++ .../runtime-dashboard/examples/record_demo.rs | 11 +- .../runtime-dashboard/examples/remote_tui.rs | 13 + crates/runtime-dashboard/examples/tui_demo.rs | 295 +++++++ crates/runtime-dashboard/src/actors_html.rs | 739 ++++++++++++++++++ crates/runtime-dashboard/src/collector.rs | 60 ++ .../runtime-dashboard/src/dashboard_html.rs | 162 +++- crates/runtime-dashboard/src/investigate.rs | 498 ++++++++++++ crates/runtime-dashboard/src/layer.rs | 24 +- crates/runtime-dashboard/src/lib.rs | 60 +- crates/runtime-dashboard/src/server.rs | 41 +- crates/runtime-dashboard/src/tui/app.rs | 357 +++++++++ crates/runtime-dashboard/src/tui/event.rs | 86 ++ crates/runtime-dashboard/src/tui/mod.rs | 191 +++++ .../runtime-dashboard/src/tui/sse_client.rs | 238 ++++++ crates/runtime-dashboard/src/tui/types.rs | 38 + crates/runtime-dashboard/src/tui/ui.rs | 473 +++++++++++ crates/swactor-python/src/lib.rs | 3 + src/actor.rs | 10 +- src/delivery.rs | 1 + src/runtime.rs | 58 +- src/stats.rs | 33 + src/worker.rs | 40 +- 30 files changed, 4257 insertions(+), 107 deletions(-) create mode 100644 crates/runtime-dashboard/AGENTS.md create mode 100644 crates/runtime-dashboard/examples/investigate_demo.rs create mode 100644 crates/runtime-dashboard/examples/remote_tui.rs create mode 100644 crates/runtime-dashboard/examples/tui_demo.rs create mode 100644 crates/runtime-dashboard/src/actors_html.rs create mode 100644 crates/runtime-dashboard/src/collector.rs create mode 100644 crates/runtime-dashboard/src/investigate.rs create mode 100644 crates/runtime-dashboard/src/tui/app.rs create mode 100644 crates/runtime-dashboard/src/tui/event.rs create mode 100644 crates/runtime-dashboard/src/tui/mod.rs create mode 100644 crates/runtime-dashboard/src/tui/sse_client.rs create mode 100644 crates/runtime-dashboard/src/tui/types.rs create mode 100644 crates/runtime-dashboard/src/tui/ui.rs diff --git a/Cargo.lock b/Cargo.lock index 701d83d..ee14054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -35,24 +41,60 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chunked_transfer" version = "1.5.0" @@ -111,6 +153,20 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "criterion" version = "0.5.1" @@ -123,7 +179,7 @@ dependencies = [ "clap", "criterion-plot", "is-terminal", - "itertools", + "itertools 0.10.5", "num-traits", "once_cell", "oorandom", @@ -144,7 +200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -181,12 +237,94 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "ctrlc" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "either" version = "1.15.0" @@ -199,6 +337,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.2.17" @@ -232,6 +386,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -256,6 +421,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.13.0" @@ -263,7 +434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -275,6 +446,19 @@ dependencies = [ "rustversion", ] +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -283,7 +467,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -295,6 +479,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -323,12 +516,36 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "memchr" version = "2.7.6" @@ -344,13 +561,37 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -362,6 +603,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.3" @@ -374,6 +630,35 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -495,6 +780,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.11.0" @@ -515,6 +821,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.3" @@ -548,6 +863,10 @@ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" name = "runtime-dashboard" version = "0.1.0" dependencies = [ + "crossbeam-queue", + "crossterm", + "ctrlc", + "ratatui", "serde", "serde_json", "swactor", @@ -556,12 +875,31 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -571,6 +909,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -632,12 +976,77 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "swactor" version = "0.1.0" @@ -829,6 +1238,35 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unindent" version = "0.2.4" @@ -912,21 +1350,52 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -936,6 +1405,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index 0200dd9..6d8a8d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,7 @@ harness = false [[bench]] name = "mt_benchmarks" harness = false + +[[example]] +name = "tcp_ping_pong" +required-features = ["transport"] diff --git a/crates/runtime-dashboard/AGENTS.md b/crates/runtime-dashboard/AGENTS.md new file mode 100644 index 0000000..962590d --- /dev/null +++ b/crates/runtime-dashboard/AGENTS.md @@ -0,0 +1,175 @@ +# Runtime Dashboard — Agent Diagnostic Interface + +## Investigate Protocol + +There is a line-oriented diagnostic protocol for programmatic runtime +investigation. It reads text commands from stdin and writes JSON responses +to stdout (one object per line). Human-readable output goes to stderr. + +### Launching + +```bash +cargo run -p runtime-dashboard --example investigate_demo +``` + +Or programmatically against any running runtime: + +```rust +use runtime_dashboard::investigate::run_investigate; +run_investigate(runtime_arc)?; // blocks on stdin +``` + +### Response Envelope + +Every response is a single JSON object on one line: + +```json +{"ok": true, "command": "overview", "data": { ... }} +{"ok": false, "command": "bogus", "error": "unknown command `bogus` — try `help`"} +``` + +### Commands + +#### overview +Overall runtime summary. +``` +→ overview +← {"ok":true,"command":"overview","data":{ + "workers": 4, + "actors": 120, + "total_messages_processed": 584210, + "total_mailbox_depth": 37, + "total_panics": 0, + "total_type_mismatches": 0, + "sends": {"local": 312000, "cross_worker": 271000, "inbox": 1210} + }} +``` + +#### workers +All workers with per-worker counters. +``` +→ workers +← {"ok":true,"command":"workers","data":[ + {"id":0,"actors":30,"mailbox_depth":12,"messages_processed":146000, + "local_sends":78000,"cross_sends":67000,"inbox_sends":300, + "type_mismatches":0,"panics":0}, + ... + ]} +``` + +#### worker <id> +Single worker detail including tick-phase timing and its actors. +``` +→ worker 2 +← {"ok":true,"command":"worker","data":{ + "id": 2, "actors": 30, "mailbox_depth": 8, + "messages_processed": 148000, + "local_sends": 80000, "cross_sends": 67500, "inbox_sends": 500, + "type_mismatches": 0, "panics": 0, + "tick_phases": { + "ticks": 412, + "active_pct": 78.4, + "avg_tick_us": 23.7, + "phases_us": [120, 980, 7200, 90, 1100, 280], + "phases_pct": [1.2, 10.0, 73.6, 0.9, 11.2, 2.9] + }, + "actor_details": [ + {"address": "a1b2c3d4…", "mailbox_depth": 3, "last_msg_type": "MyMsg"}, + ... + ] + }} +``` + +The six tick phases (indices 0–5): +0. spawn_drain — draining spawn channel +1. transfer_drain — draining cross-worker transfer channel +2. tick_all — processing actor mailboxes (main work) +3. spawn_drain_2 — draining spawns created during tick +4. pending_local — delivering messages buffered within the worker +5. stats_publish — updating shared stat counters + +#### actors [--sort mailbox|worker|address] [--limit N] [--worker W] +Actor listing with sorting, limit, and worker filter. +``` +→ actors --sort mailbox --limit 5 +→ actors --worker 0 --sort address +→ actors --limit 20 +``` + +#### actor <hex_prefix> +Find actors whose address starts with the given hex prefix. +``` +→ actor a1b2 +← {"ok":true,"command":"actor","data":{ + "prefix": "a1b2", + "matches": 1, + "actors": [{"address": "a1b2c3d4…", "address_full": "a1b2c3d4...(64 hex chars)", "worker_id": 2, "mailbox_depth": 3, "last_msg_type": "MyMsg"}] + }} +``` + +#### hot [N] +Top N actors by mailbox depth (default 10). Use this to find backpressure. +``` +→ hot 5 +``` + +#### phases [worker_id] +Tick-phase timing breakdown. Without an argument returns all workers. +``` +→ phases +→ phases 2 +``` + +#### diff <seconds> +Takes two snapshots separated by N seconds (max 30) and reports deltas. +This is the primary throughput measurement tool. +``` +→ diff 2 +← {"ok":true,"command":"diff","data":{ + "elapsed_s": 2.001, + "actors_before": 120, "actors_after": 132, + "delta_messages": 8432, + "msg_per_sec": 4213.9, + "delta_local_sends": 4500, + "delta_cross_sends": 3900, + "mailbox_before": 37, "mailbox_after": 42, + "per_worker": [ + {"worker_id": 0, "delta_messages": 2100, "msg_per_sec": 1049.5, + "actors_before": 30, "actors_after": 33, + "mailbox_before": 12, "mailbox_after": 14}, + ... + ] + }} +``` + +#### help +Returns all commands with usage strings. + +#### quit +Exits the session. + +### Investigation Playbook + +When diagnosing a runtime, a useful sequence: + +1. `overview` — get the lay of the land +2. `diff 2` — measure live throughput and detect growth +3. `hot 10` — find actors with deepest mailboxes (backpressure) +4. `workers` — compare per-worker load distribution +5. `worker ` — drill into the busiest worker, check phase breakdown +6. `phases` — check if workers are spending time in unexpected phases +7. `actors --worker --sort mailbox` — find hot actors on that worker +8. `actor ` — get full address and type for a specific actor + +### Key Metrics to Watch + +| Symptom | Check | Meaning | +|---------|-------|---------| +| High mailbox_depth | `hot 10` | Actor can't keep up — backpressure | +| Uneven msg_per_sec across workers | `diff 2` per_worker | Load imbalance | +| High cross_sends vs local_sends | `overview` sends | Actors that talk are on different workers | +| active_pct near 100% | `phases ` | Worker is saturated | +| High phase 1+4 % vs phase 2 | `phases ` | Delivery overhead dominates processing | +| Growing actors_after vs actors_before | `diff 5` | Unbounded actor spawning | +| type_mismatches > 0 | `overview` | Messages routed to wrong actor type | +| panics > 0 | `overview` | Actor handlers are panicking | diff --git a/crates/runtime-dashboard/Cargo.toml b/crates/runtime-dashboard/Cargo.toml index 76b4102..37eeaa5 100644 --- a/crates/runtime-dashboard/Cargo.toml +++ b/crates/runtime-dashboard/Cargo.toml @@ -10,3 +10,21 @@ tracing-subscriber = { version = "0.3", features = ["registry"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tiny_http = "0.12" +crossbeam-queue = "0.3.12" +ratatui = { version = "0.29", optional = true, default-features = false, features = ["crossterm"] } +crossterm = { version = "0.28", optional = true } + +[dev-dependencies] +ctrlc = "3" + +[features] +default = [] +tui = ["dep:ratatui", "dep:crossterm"] + +[[example]] +name = "tui_demo" +required-features = ["tui"] + +[[example]] +name = "remote_tui" +required-features = ["tui"] diff --git a/crates/runtime-dashboard/README.md b/crates/runtime-dashboard/README.md index 149396b..23ca63b 100644 --- a/crates/runtime-dashboard/README.md +++ b/crates/runtime-dashboard/README.md @@ -1,3 +1,14 @@ # runtime-dashboard -Visual dashboard for the swactor runtime. \ No newline at end of file +Visual dashboard for the swactor runtime. + +## usage + +Run the demo, from swactor root: +```bash +# start the runtime +cargo run -p runtime-dashboard --example dashboard_demo + +# (optional) run the TUI +cargo run -p runtime-dashboard --features tui --example tui_demo +``` \ No newline at end of file diff --git a/crates/runtime-dashboard/examples/bench_dashboard.rs b/crates/runtime-dashboard/examples/bench_dashboard.rs index e249241..a227b9b 100644 --- a/crates/runtime-dashboard/examples/bench_dashboard.rs +++ b/crates/runtime-dashboard/examples/bench_dashboard.rs @@ -5,6 +5,7 @@ use swactor::actor::{ActorAddress, ActorInterface, Ctx}; use swactor::config::{BackoffPolicy, RuntimeConfig}; use swactor::runtime::Runtime; +use runtime_dashboard::collector::StatsCollector; use runtime_dashboard::{start_dashboard, DashboardConfig}; // --------------------------------------------------------------------------- @@ -102,10 +103,12 @@ fn run_for(duration: Duration, mut tick: impl FnMut()) { fn scenario_single_actor(dash: &runtime_dashboard::DashboardHandle) { eprintln!(" [1/4] Single-actor bombardment (5s)"); - let rt = Runtime::new(bench_config(4, 64, 100_000)); + let collector = StatsCollector::new(4); + let mut rt = Runtime::new(bench_config(4, 64, 100_000)); + rt.set_stats_hook(collector.clone()); let addr = rt.spawn(SinkActor::new()).unwrap(); let handle = rt.run().unwrap(); - dash.set_runtime(handle.runtime.clone()); + dash.set_runtime(handle.runtime.clone(), collector); run_for(Duration::from_secs(5), || { for _ in 0..100 { @@ -120,12 +123,14 @@ fn scenario_single_actor(dash: &runtime_dashboard::DashboardHandle) { fn scenario_multi_actor(dash: &runtime_dashboard::DashboardHandle) { eprintln!(" [2/4] Multi-actor fan-out (5s)"); - let rt = Runtime::new(bench_config(4, 128, 10_000)); + let collector = StatsCollector::new(4); + let mut rt = Runtime::new(bench_config(4, 128, 10_000)); + rt.set_stats_hook(collector.clone()); let addrs: Vec<_> = (0..50) .map(|_| rt.spawn(SinkActor::new()).unwrap()) .collect(); let handle = rt.run().unwrap(); - dash.set_runtime(handle.runtime.clone()); + dash.set_runtime(handle.runtime.clone(), collector); run_for(Duration::from_secs(5), || { for &addr in &addrs { @@ -143,7 +148,9 @@ fn scenario_multi_actor(dash: &runtime_dashboard::DashboardHandle) { fn scenario_ring(dash: &runtime_dashboard::DashboardHandle) { eprintln!(" [3/4] Ring topology (5s)"); let ring_size = 100; - let rt = Runtime::new(bench_config(4, ring_size + 64, 1_024)); + let collector = StatsCollector::new(4); + let mut rt = Runtime::new(bench_config(4, ring_size + 64, 1_024)); + rt.set_stats_hook(collector.clone()); // Build ring backwards: last spawned actor is the entry point let mut addrs = Vec::with_capacity(ring_size); @@ -163,7 +170,7 @@ fn scenario_ring(dash: &runtime_dashboard::DashboardHandle) { let entry = *addrs.last().unwrap(); let handle = rt.run().unwrap(); - dash.set_runtime(handle.runtime.clone()); + dash.set_runtime(handle.runtime.clone(), collector); run_for(Duration::from_secs(5), || { let _ = handle.runtime.send_to(entry, RingMsg); @@ -176,10 +183,12 @@ fn scenario_ring(dash: &runtime_dashboard::DashboardHandle) { fn scenario_spawn_storm(dash: &runtime_dashboard::DashboardHandle) { eprintln!(" [4/4] Spawn storm (5s)"); - let rt = Runtime::new(bench_config(4, 50_000, 1_024)); + let collector = StatsCollector::new(4); + let mut rt = Runtime::new(bench_config(4, 50_000, 1_024)); + rt.set_stats_hook(collector.clone()); let spawner = rt.spawn(SpawnerActor::new()).unwrap(); let handle = rt.run().unwrap(); - dash.set_runtime(handle.runtime.clone()); + dash.set_runtime(handle.runtime.clone(), collector); run_for(Duration::from_secs(5), || { let _ = handle.runtime.send_to(spawner, SpawnCmd); diff --git a/crates/runtime-dashboard/examples/dashboard_demo.rs b/crates/runtime-dashboard/examples/dashboard_demo.rs index 649853a..994942f 100644 --- a/crates/runtime-dashboard/examples/dashboard_demo.rs +++ b/crates/runtime-dashboard/examples/dashboard_demo.rs @@ -7,6 +7,7 @@ use swactor::actor::{ActorAddress, ActorInterface, Ctx}; use swactor::config::RuntimeConfig; use swactor::runtime::Runtime; +use runtime_dashboard::collector::StatsCollector; use runtime_dashboard::{start_dashboard, DashboardConfig}; // ── Demo actors ───────────────────────────────────────────────────────── @@ -31,7 +32,7 @@ impl ActorInterface for PingActor { fn handle(&mut self, ctx: &Ctx, msg: Ping) { self.count += 1; // Forward to the target — creates cross-worker traffic - if self.count < 200 { + if self.count < 10_000 { let _ = ctx.send(msg.0, Ping(ctx.self_addr())); } } @@ -67,12 +68,10 @@ fn main() { // Handle Ctrl+C gracefully { let stop = Arc::clone(&stop); - let _ = std::panic::catch_unwind(|| { - // Try to register a signal handler; fall back to running until killed - unsafe { - libc_signal(2, move || stop.store(true, Ordering::Relaxed)); - } - }); + ctrlc::set_handler(move || { + stop.store(true, Ordering::Relaxed); + }) + .expect("failed to set Ctrl+C handler"); } let dash = start_dashboard(DashboardConfig { @@ -81,12 +80,16 @@ fn main() { }); dash.install_tracing(); - let rt = Runtime::new(RuntimeConfig { - num_threads: 4, + let num_threads = 4; + let collector = StatsCollector::new(num_threads); + + let mut rt = Runtime::new(RuntimeConfig { + num_threads, max_actors: 1024, channel_buffer_size: 2000, ..Default::default() }); + rt.set_stats_hook(collector.clone()); // Spawn ping actors for cross-worker traffic let mut ping_addrs = Vec::new(); @@ -103,7 +106,7 @@ fn main() { } let handle = rt.run().expect("failed to start runtime"); - dash.set_runtime(handle.runtime.clone()); + dash.set_runtime(handle.runtime.clone(), collector); eprintln!("Dashboard at http://localhost:9090 — press Ctrl+C to stop"); @@ -121,7 +124,7 @@ fn main() { } // Periodically spawn more actors - if round % 150 == 75 && counter_addrs.len() < 200 { + if round % 150 == 75 && counter_addrs.len() < 500 { for _ in 0..8 { match handle.runtime.spawn(CounterActor::new()) { Ok(addr) => counter_addrs.push(addr), @@ -147,9 +150,3 @@ fn main() { dash.shutdown(); handle.join(); } - -// Minimal signal handling without external deps -unsafe fn libc_signal(_sig: i32, _handler: impl FnOnce()) { - // This is a no-op fallback; the loop checks the AtomicBool - // In practice, Ctrl+C will terminate the process -} diff --git a/crates/runtime-dashboard/examples/investigate_demo.rs b/crates/runtime-dashboard/examples/investigate_demo.rs new file mode 100644 index 0000000..f8c52ef --- /dev/null +++ b/crates/runtime-dashboard/examples/investigate_demo.rs @@ -0,0 +1,122 @@ +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use swactor::actor::{ActorAddress, ActorInterface, Ctx}; +use swactor::config::RuntimeConfig; +use swactor::runtime::Runtime; + +use runtime_dashboard::collector::StatsCollector; +use runtime_dashboard::investigate::run_investigate; + +// ── Demo actors (same as tui_demo) ───────────────────────────────────── + +#[derive(Clone)] +struct Ping(ActorAddress); + +struct PingActor { + count: u32, + limit: u32, +} + +impl PingActor { + fn new(limit: u32) -> Self { + Self { count: 0, limit } + } +} + +impl ActorInterface for PingActor { + type Incoming = Ping; + type Response = (); + + fn handle(&mut self, ctx: &Ctx, msg: Ping) { + self.count += 1; + if self.count < self.limit { + let _ = ctx.send(msg.0, Ping(ctx.self_addr())); + } + } +} + +#[derive(Clone)] +struct Tick; + +struct CounterActor { + ticks: u64, +} + +impl CounterActor { + fn new() -> Self { + Self { ticks: 0 } + } +} + +impl ActorInterface for CounterActor { + type Incoming = Tick; + type Response = (); + + fn handle(&mut self, _ctx: &Ctx, _msg: Tick) { + self.ticks += 1; + } +} + +// ── Main ──────────────────────────────────────────────────────────────── + +fn main() -> std::io::Result<()> { + let num_threads = 4; + let collector = StatsCollector::new(num_threads); + + let mut rt = Runtime::new(RuntimeConfig { + num_threads, + max_actors: 1024, + channel_buffer_size: 2000, + ..Default::default() + }); + rt.set_stats_hook(collector.clone()); + + // Spawn some actors + let mut ping_addrs = Vec::new(); + for _ in 0..16 { + ping_addrs.push(rt.spawn(PingActor::new(500)).unwrap()); + } + + let mut counter_addrs = Vec::new(); + for _ in 0..40 { + counter_addrs.push(rt.spawn(CounterActor::new()).unwrap()); + } + + let handle = rt.run().expect("failed to start runtime"); + let runtime = Arc::clone(&handle.runtime); + + // Wire up ping-pong + for i in 0..ping_addrs.len() { + let target = ping_addrs[(i + 1) % ping_addrs.len()]; + let _ = runtime.send_to(ping_addrs[i], Ping(target)); + } + + // Feeder thread + let rt_feeder = Arc::clone(&runtime); + let ping_clone = ping_addrs.clone(); + thread::spawn(move || { + let mut round: u64 = 0; + loop { + for addr in &counter_addrs { + let _ = rt_feeder.send_to(*addr, Tick); + } + if round % 40 == 0 && round > 0 { + for i in 0..ping_clone.len() { + let target = ping_clone[(i + 1) % ping_clone.len()]; + let _ = rt_feeder.send_to(ping_clone[i], Ping(target)); + } + } + round += 1; + thread::sleep(Duration::from_millis(100)); + } + }); + + // Blocks on stdin — send commands, get JSON back + run_investigate(runtime, collector)?; + + handle.shutdown(); + handle.join(); + Ok(()) +} diff --git a/crates/runtime-dashboard/examples/record_demo.rs b/crates/runtime-dashboard/examples/record_demo.rs index 99cd5b4..9e6d837 100644 --- a/crates/runtime-dashboard/examples/record_demo.rs +++ b/crates/runtime-dashboard/examples/record_demo.rs @@ -5,6 +5,7 @@ use swactor::actor::{ActorAddress, ActorInterface, Ctx}; use swactor::config::RuntimeConfig; use swactor::runtime::Runtime; +use runtime_dashboard::collector::StatsCollector; use runtime_dashboard::{start_dashboard, DashboardConfig}; // ── Demo actors ───────────────────────────────────────────────────────── @@ -56,12 +57,16 @@ fn main() { }); dash.install_tracing(); - let rt = Runtime::new(RuntimeConfig { - num_threads: 4, + let num_threads = 4; + let collector = StatsCollector::new(num_threads); + + let mut rt = Runtime::new(RuntimeConfig { + num_threads, max_actors: 512, channel_buffer_size: 1000, ..Default::default() }); + rt.set_stats_hook(collector.clone()); let mut ping_addrs = Vec::new(); for _ in 0..12 { @@ -76,7 +81,7 @@ fn main() { } let handle = rt.run().expect("failed to start runtime"); - dash.set_runtime(handle.runtime.clone()); + dash.set_runtime(handle.runtime.clone(), collector); eprintln!("Recording trace for 10 seconds..."); eprintln!("Dashboard at http://localhost:9090"); diff --git a/crates/runtime-dashboard/examples/remote_tui.rs b/crates/runtime-dashboard/examples/remote_tui.rs new file mode 100644 index 0000000..b30f3be --- /dev/null +++ b/crates/runtime-dashboard/examples/remote_tui.rs @@ -0,0 +1,13 @@ +use runtime_dashboard::tui::types::RuntimeEndpoint; +use runtime_dashboard::tui::{TuiConfig, start_tui_remote}; + +fn main() -> std::io::Result<()> { + let url = std::env::args() + .nth(1) + .unwrap_or_else(|| "http://localhost:9090".into()); + + let endpoint = RuntimeEndpoint::from_url(&url); + eprintln!("Connecting to {} ...", endpoint); + + start_tui_remote(endpoint, TuiConfig::default()) +} diff --git a/crates/runtime-dashboard/examples/tui_demo.rs b/crates/runtime-dashboard/examples/tui_demo.rs new file mode 100644 index 0000000..9dd3842 --- /dev/null +++ b/crates/runtime-dashboard/examples/tui_demo.rs @@ -0,0 +1,295 @@ +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use swactor::actor::{ActorAddress, ActorInterface, Ctx}; +use swactor::config::RuntimeConfig; +use swactor::runtime::Runtime; + +use runtime_dashboard::collector::StatsCollector; +use runtime_dashboard::tui::{TuiConfig, start_tui}; + +// ── Demo actors ───────────────────────────────────────────────────────── + +/// Ping-pong actor: bounces messages back and forth creating cross-worker traffic. +#[derive(Clone)] +struct Ping(ActorAddress); + +struct PingActor { + count: u32, + limit: u32, +} + +impl PingActor { + fn new(limit: u32) -> Self { + Self { count: 0, limit } + } +} + +impl ActorInterface for PingActor { + type Incoming = Ping; + type Response = (); + + fn handle(&mut self, ctx: &Ctx, msg: Ping) { + self.count += 1; + if self.count < self.limit { + let _ = ctx.send(msg.0, Ping(ctx.self_addr())); + } + } +} + +/// Simple counter that tallies tick messages. +#[derive(Clone)] +struct Tick; + +struct CounterActor { + ticks: u64, +} + +impl CounterActor { + fn new() -> Self { + Self { ticks: 0 } + } +} + +impl ActorInterface for CounterActor { + type Incoming = Tick; + type Response = (); + + fn handle(&mut self, _ctx: &Ctx, _msg: Tick) { + self.ticks += 1; + } +} + +/// Fan-out actor: on each message, forwards to all targets — amplifies traffic. +#[derive(Clone)] +struct Fanout(Vec); + +struct FanoutActor { + targets: Vec, +} + +impl ActorInterface for FanoutActor { + type Incoming = Fanout; + type Response = (); + + fn handle(&mut self, ctx: &Ctx, msg: Fanout) { + self.targets = msg.0; + for &t in &self.targets { + let _ = ctx.send(t, Tick); + } + } +} + +/// Chain actor: receives a hop count, decrements, and forwards to the next in chain. +#[derive(Clone)] +struct Hop { + remaining: u32, + chain: Vec, + index: usize, +} + +struct ChainActor; + +impl ActorInterface for ChainActor { + type Incoming = Hop; + type Response = (); + + fn handle(&mut self, ctx: &Ctx, msg: Hop) { + if msg.remaining > 0 { + let next_idx = (msg.index + 1) % msg.chain.len(); + let _ = ctx.send( + msg.chain[next_idx], + Hop { + remaining: msg.remaining - 1, + chain: msg.chain, + index: next_idx, + }, + ); + } + } +} + +// ── Main ──────────────────────────────────────────────────────────────── + +fn main() -> std::io::Result<()> { + let num_threads = 8; + let collector = StatsCollector::new(num_threads); + + let mut rt = Runtime::new(RuntimeConfig { + num_threads, + max_actors: 4096, + channel_buffer_size: 4000, + ..Default::default() + }); + rt.set_stats_hook(collector.clone()); + + // ── Ping-pong pairs (cross-worker bouncing) ──────────────────────── + let mut ping_addrs = Vec::new(); + for _ in 0..32 { + ping_addrs.push(rt.spawn(PingActor::new(500)).unwrap()); + } + + // ── Counter actors (sustained tick traffic) ──────────────────────── + let mut counter_addrs: Vec = Vec::new(); + for _ in 0..60 { + counter_addrs.push(rt.spawn(CounterActor::new()).unwrap()); + } + + // ── Fan-out hubs (traffic amplifiers) ────────────────────────────── + let mut fanout_addrs = Vec::new(); + for _ in 0..4 { + fanout_addrs.push( + rt.spawn(FanoutActor { + targets: Vec::new(), + }) + .unwrap(), + ); + } + + // ── Chain rings (message relay loops) ────────────────────────────── + let mut chain_addrs = Vec::new(); + for _ in 0..24 { + chain_addrs.push(rt.spawn(ChainActor).unwrap()); + } + + let handle = rt.run().expect("failed to start runtime"); + let runtime = Arc::clone(&handle.runtime); + + // Wire up ping-pong chains + for i in 0..ping_addrs.len() { + let target = ping_addrs[(i + 1) % ping_addrs.len()]; + let _ = runtime.send_to(ping_addrs[i], Ping(target)); + } + + // Wire up fan-out hubs: each hub fans to a slice of counter actors + let chunk_size = counter_addrs.len() / fanout_addrs.len().max(1); + for (i, &hub) in fanout_addrs.iter().enumerate() { + let start = i * chunk_size; + let end = (start + chunk_size).min(counter_addrs.len()); + let targets: Vec<_> = counter_addrs[start..end].to_vec(); + let _ = runtime.send_to(hub, Fanout(targets)); + } + + // Kick off chain rings: 3 rings of 8 actors each + for ring_start in (0..chain_addrs.len()).step_by(8) { + let ring: Vec<_> = chain_addrs[ring_start..ring_start + 8].to_vec(); + let _ = runtime.send_to( + ring[0], + Hop { + remaining: 200, + chain: ring, + index: 0, + }, + ); + } + + // ── Feeder threads ───────────────────────────────────────────────── + + // Thread 1: tick all counters + periodically spawn more + let rt1 = Arc::clone(&runtime); + let fanout_addrs_clone = fanout_addrs.clone(); + thread::spawn(move || { + let mut counter_addrs = counter_addrs; + let mut round: u64 = 0; + loop { + // Tick every counter + for addr in &counter_addrs { + let _ = rt1.send_to(*addr, Tick); + } + + // Periodically spawn more counters (grow from 60 up to 400) + if round % 50 == 25 && counter_addrs.len() < 400 { + let mut new_addrs = Vec::new(); + for _ in 0..12 { + match rt1.spawn(CounterActor::new()) { + Ok(addr) => new_addrs.push(addr), + Err(_) => break, + } + } + // Re-wire fan-out hubs with expanded target list + let chunk = new_addrs.len() / fanout_addrs_clone.len().max(1); + for (i, &hub) in fanout_addrs_clone.iter().enumerate() { + let start = i * chunk; + let end = (start + chunk).min(new_addrs.len()); + if start < end { + let targets: Vec<_> = new_addrs[start..end].to_vec(); + let _ = rt1.send_to(hub, Fanout(targets)); + } + } + counter_addrs.extend(new_addrs); + } + + round += 1; + thread::sleep(Duration::from_millis(100)); + } + }); + + // Thread 2: re-kick ping chains + chain rings periodically + let rt2 = Arc::clone(&runtime); + let ping_clone = ping_addrs.clone(); + let chain_clone = chain_addrs.clone(); + thread::spawn(move || { + let mut round: u64 = 0; + loop { + // Re-kick ping-pong chains + if round % 40 == 0 { + for i in 0..ping_clone.len() { + let target = ping_clone[(i + 1) % ping_clone.len()]; + let _ = rt2.send_to(ping_clone[i], Ping(target)); + } + } + + // Re-kick chain rings + if round % 30 == 0 { + for ring_start in (0..chain_clone.len()).step_by(8) { + let ring: Vec<_> = chain_clone[ring_start..ring_start + 8].to_vec(); + let _ = rt2.send_to( + ring[0], + Hop { + remaining: 200, + chain: ring, + index: 0, + }, + ); + } + } + + // Periodically spawn short-lived ping bursts + if round % 60 == 30 { + let mut burst = Vec::new(); + for _ in 0..8 { + match rt2.spawn(PingActor::new(50)) { + Ok(addr) => burst.push(addr), + Err(_) => break, + } + } + for i in 0..burst.len() { + let target = burst[(i + 1) % burst.len()]; + let _ = rt2.send_to(burst[i], Ping(target)); + } + } + + round += 1; + thread::sleep(Duration::from_millis(150)); + } + }); + + // Thread 3: fan-out re-trigger (keeps hubs active) + let rt3 = Arc::clone(&runtime); + let fanout_clone = fanout_addrs.clone(); + thread::spawn(move || loop { + for &hub in &fanout_clone { + // Re-send so hub forwards again to its targets + let _ = rt3.send_to(hub, Fanout(Vec::new())); + } + thread::sleep(Duration::from_millis(80)); + }); + + // This blocks until the user presses 'q' + start_tui(runtime, collector, TuiConfig::default())?; + + handle.shutdown(); + handle.join(); + Ok(()) +} diff --git a/crates/runtime-dashboard/src/actors_html.rs b/crates/runtime-dashboard/src/actors_html.rs new file mode 100644 index 0000000..bf2c12d --- /dev/null +++ b/crates/runtime-dashboard/src/actors_html.rs @@ -0,0 +1,739 @@ +pub const ACTORS_HTML: &str = r##" + + + + +Swactor Runtime – Actors + + + +
+
+

+ Swactor Runtime Dashboard + + REPLAY +

+ +
+
+ + + +
+
+
+
+
+ +
+ +
+

Actor Stats

+
+
0
Total Actors
+
0
Avg Mailbox
+
0
Max Mailbox
+
0
Active Workers
+
+
+ + +
+

Mailbox Depth Distribution

+ +
+ + +
+

Actors per Worker

+ +
+ + +
+

All Actors

+
+ + +
+
+ + + + + + + + + + + + +
Address Worker Mailbox Msgs Last MsgDepth
+
+
+ + +
+
+

Actor Detail

+ +
+
+
Full Address
+
Worker
+
Mailbox Depth
+
Messages Processed
+
Last Message Type
+
Status
+
+

Mailbox Depth History

+ +
+
+ + + + +"##; diff --git a/crates/runtime-dashboard/src/collector.rs b/crates/runtime-dashboard/src/collector.rs new file mode 100644 index 0000000..59df4d0 --- /dev/null +++ b/crates/runtime-dashboard/src/collector.rs @@ -0,0 +1,60 @@ +//! Lock-free-ish stats collector that implements [`StatsHook`]. +//! +//! Workers push per-actor snapshots here; dashboard code reads them back +//! via [`actor_details()`](StatsCollector::actor_details) or +//! [`enrich()`](StatsCollector::enrich). + +use std::sync::{Arc, RwLock}; + +use swactor::stats::{ActorInfo, ActorSnapshot, RuntimeStats, StatsHook}; + +/// Collects per-actor snapshots pushed by worker threads. +/// +/// Create with [`new()`](Self::new), pass to both +/// [`Runtime::set_stats_hook()`](swactor::runtime::Runtime::set_stats_hook) +/// and the dashboard. +pub struct StatsCollector { + /// One slot per worker — writers never contend with each other. + slots: Vec>>, +} + +impl StatsCollector { + pub fn new(num_workers: usize) -> Arc { + let slots = (0..num_workers) + .map(|_| RwLock::new(Vec::new())) + .collect(); + Arc::new(Self { slots }) + } + + /// Collect all per-actor details across workers. + pub fn actor_details(&self) -> Vec { + let mut out = Vec::new(); + for slot in &self.slots { + let guard = slot.read().unwrap(); + out.extend(guard.iter().cloned()); + } + out + } + + /// Patch `actor_details` into an existing [`RuntimeStats`]. + pub fn enrich(&self, stats: &mut RuntimeStats) { + stats.actor_details = self.actor_details(); + } +} + +impl StatsHook for StatsCollector { + fn on_tick(&self, worker_id: usize, snapshots: &[ActorSnapshot]) { + if let Some(slot) = self.slots.get(worker_id) { + let mut guard = slot.write().unwrap(); + guard.clear(); + guard.extend(snapshots.iter().map(|s| ActorInfo { + address: s.address, + worker_id, + mailbox_depth: s.mailbox_depth, + last_msg_type: s.last_msg_type.map(|t| t.to_string()), + messages_processed: s.messages_processed, + poisoned: s.poisoned, + })); + } + } +} diff --git a/crates/runtime-dashboard/src/dashboard_html.rs b/crates/runtime-dashboard/src/dashboard_html.rs index d3b0519..4ec3d92 100644 --- a/crates/runtime-dashboard/src/dashboard_html.rs +++ b/crates/runtime-dashboard/src/dashboard_html.rs @@ -33,6 +33,15 @@ pub const DASHBOARD_HTML: &str = r##" } .replay-badge.visible { display: inline-block; } + .header-left { display: flex; align-items: center; } + .nav-links { display: flex; gap: 4px; margin-left: 20px; } + .nav-link { + color: #888; text-decoration: none; font-size: 12px; + padding: 4px 10px; border-radius: 3px; transition: color 0.2s; + } + .nav-link:hover { color: #e0e0e0; } + .nav-link.active { color: #fff; background: #2a2d3e; } + .header-right { display: flex; align-items: center; gap: 12px; } .progress-bar-wrap { @@ -93,6 +102,27 @@ pub const DASHBOARD_HTML: &str = r##" .level-DEBUG { color: #2196f3; } .level-TRACE { color: #666; } + .worker-detail-panel { grid-column: 1 / -1; } + .worker-detail-scroll { max-height: 360px; overflow-y: auto; } + .worker-group { margin-bottom: 8px; border: 1px solid #2a2d3e; border-radius: 4px; overflow: hidden; } + .worker-group-header { + display: flex; align-items: center; justify-content: space-between; + padding: 7px 12px; background: #1c1f2e; cursor: pointer; user-select: none; + } + .worker-group-header:hover { background: #22253a; } + .worker-group-header .wid { font-weight: 700; } + .worker-group-header .summary { color: #888; font-size: 11px; } + .worker-group-header .toggle { color: #555; font-size: 14px; } + .worker-group-body { display: none; } + .worker-group.open .worker-group-body { display: block; } + .worker-group-body table { width: 100%; border-collapse: collapse; } + .worker-group-body th, .worker-group-body td { + padding: 3px 10px; text-align: left; border-bottom: 1px solid #1c1f2e; font-size: 11px; + } + .worker-group-body th { color: #888; font-weight: 500; background: #161822; } + .msg-type { color: #4caf50; } + .msg-type.none { color: #555; font-style: italic; } + ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: #0f1117; } ::-webkit-scrollbar-thumb { background: #2a2d3e; border-radius: 3px; } @@ -100,11 +130,17 @@ pub const DASHBOARD_HTML: &str = r##"
-

- Swactor Runtime Dashboard - - REPLAY -

+
+

+ Swactor Runtime Dashboard + + REPLAY +

+ +
@@ -142,6 +178,11 @@ pub const DASHBOARD_HTML: &str = r##"
+
+

Worker Details

+
+
+

Activity Log

@@ -156,10 +197,11 @@ pub const DASHBOARD_HTML: &str = r##"