refactor: consolidate crates #39

Merged
zacheryasc merged 1 commit from consolidate into master 2026-02-13 17:34:02 +00:00
58 changed files with 89 additions and 241 deletions

40
Cargo.lock generated
View file

@ -1668,17 +1668,6 @@ dependencies = [
"libc",
]
[[package]]
name = "node"
version = "0.1.0"
dependencies = [
"clap",
"ctrlc",
"distribution",
"runtime-dashboard",
"swactor",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@ -2294,6 +2283,7 @@ dependencies = [
name = "runtime-dashboard"
version = "0.1.0"
dependencies = [
"clap",
"crossbeam-queue",
"crossterm",
"ctrlc",
@ -2302,7 +2292,6 @@ dependencies = [
"serde",
"serde_json",
"swactor",
"swactor-command",
"tiny_http",
"tracing",
"tracing-subscriber",
@ -2610,15 +2599,6 @@ dependencies = [
"serde_json",
"simulation",
"swactor",
]
[[package]]
name = "simulation-dashboard"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"simulation",
"tiny_http",
"toml",
]
@ -2727,12 +2707,14 @@ dependencies = [
]
[[package]]
name = "swactor-command"
name = "swactor-bin-runner"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"proptest",
"swactor",
"swactor-std",
"wasmtime",
"wat",
]
[[package]]
@ -2743,16 +2725,6 @@ dependencies = [
"swactor",
]
[[package]]
name = "swactor-wasm-actor"
version = "0.1.0"
dependencies = [
"proptest",
"swactor",
"wasmtime",
"wat",
]
[[package]]
name = "syn"
version = "2.0.115"

View file

@ -1,5 +1,5 @@
[workspace]
members = [".", "crates/python", "crates/wasm", "crates/wasm-actor", "crates/simulation", "crates/runtime-dashboard", "crates/distribution", "crates/simulation-dashboard", "crates/std", "crates/command", "crates/node", "tests/docker"]
members = [".", "crates/python", "crates/wasm", "crates/bin-runner", "crates/simulation", "crates/runtime-dashboard", "crates/distribution", "crates/std", "tests/docker"]
exclude = ["tools/depgraph"]
[package]

View file

@ -1,7 +1,7 @@
FROM rust:1.93-slim AS builder
WORKDIR /build
COPY . .
RUN cargo build --release -p node
RUN cargo build --release -p runtime-dashboard --features node
FROM debian:bookworm-slim
COPY --from=builder /build/target/release/swactor-node /usr/local/bin/

View file

@ -1,5 +1,5 @@
[package]
name = "swactor-wasm-actor"
name = "swactor-bin-runner"
version = "0.1.0"
edition = "2024"
@ -9,5 +9,6 @@ wasmtime = "29"
[dev-dependencies]
swactor = { path = "../..", features = ["getrandom"] }
swactor-std = { path = "../std" }
wat = "1"
proptest = "1"

View file

@ -0,0 +1,8 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc f8674b306ba8187ad75bcc5a33c4a679e497a0053dc6fdb2234ccaeaae8f85b7 # shrinks to guest_idx = 0, n_msgs = 0, ticks_before = 1, ticks_after = 1
cc db98a9c0726c0477ba56ec6098a1f21518faaff4551d90e915790ce3c890dd53 # shrinks to payload = []

View file

@ -1,6 +1,7 @@
use swactor::actor::{ActorAddress, ActorInterface};
use swactor::runtime::{Ctx, Runtime, RuntimeConfig};
use swactor_wasm_actor::{ByteMessage, SharedEngine, WasmActor, WasmActorBuilder, WasmActorError};
use swactor_bin_runner::{ByteMessage, SharedEngine, WasmActor, WasmActorBuilder, WasmActorError};
use swactor_std::CtxWatching;
use proptest::prelude::*;

View file

@ -1,9 +0,0 @@
[package]
name = "swactor-command"
version = "0.1.0"
edition = "2024"
[dependencies]
swactor = { path = "../..", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View file

@ -1,15 +0,0 @@
[package]
name = "node"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "swactor-node"
path = "src/main.rs"
[dependencies]
swactor = { path = "../..", features = ["serde", "tracing", "transport"] }
distribution = { path = "../distribution" }
runtime-dashboard = { path = "../runtime-dashboard", features = ["distribution"] }
clap = { version = "4", features = ["derive"] }
ctrlc = "3"

View file

@ -14,17 +14,21 @@ crossbeam-queue = "0.3.12"
ratatui = { version = "0.29", optional = true, default-features = false, features = ["crossterm"] }
crossterm = { version = "0.28", optional = true }
distribution = { path = "../distribution", optional = true }
swactor-command = { path = "../command" }
[dependencies.ctrlc]
version = "3"
clap = { version = "4", features = ["derive"], optional = true }
ctrlc = "3"
[features]
default = ["distribution"]
tui = ["dep:ratatui", "dep:crossterm"]
distribution = ["dep:distribution"]
node = ["distribution", "dep:clap", "swactor/transport"]
[[bin]]
name = "swactor-tui"
path = "src/bin/tui.rs"
required-features = ["tui"]
[[bin]]
name = "swactor-node"
path = "src/bin/swactor-node.rs"
required-features = ["node"]

View file

@ -116,6 +116,7 @@ fn main() {
swim: swim_config,
cache_capacity: 1000,
republish_interval: 500,
..Default::default()
};
let mut driver = NodeDriver::new(node_config).expect("failed to create node driver");

View file

@ -42,7 +42,7 @@ impl StatsCollector {
}
}
impl swactor_command::StatsEnricher for StatsCollector {
impl crate::command::StatsEnricher for StatsCollector {
fn enrich(&self, stats: &mut swactor::stats::RuntimeStats) {
stats.actor_details = self.actor_details();
}

View file

@ -8,7 +8,7 @@ use std::time::{Duration, Instant};
use swactor::actor::ActorAddress;
use swactor::stats::TickTiming;
use crate::{CommandContext, CommandHandler, CommandMeta, CommandResponse};
use super::{CommandContext, CommandHandler, CommandMeta, CommandResponse};
// ─── Arg helpers ─────────────────────────────────────────────────────────────

View file

@ -2,7 +2,7 @@
use std::collections::HashMap;
use crate::CommandRequest;
use super::CommandRequest;
/// Parse a REPL text line into a [`CommandRequest`].
///

View file

@ -1,6 +1,6 @@
//! Line-oriented diagnostic protocol for LLM-driven runtime investigation.
//!
//! Delegates all command logic to the `swactor-command` crate.
//! Delegates all command logic to the `command` module.
//!
//! Send text commands on stdin, receive JSON responses on stdout (one per line).
//! All human-readable diagnostics go to stderr.
@ -23,7 +23,7 @@ use std::io::{self, BufRead, Write};
use std::sync::Arc;
use swactor::runtime::Runtime;
use swactor_command::{CommandContext, CommandRouter};
use crate::command::{CommandContext, CommandRouter};
use crate::collector::StatsCollector;
@ -47,7 +47,7 @@ pub fn run_investigate(runtime: Arc<Runtime>, collector: Arc<StatsCollector>) ->
break;
}
let req = swactor_command::parse_line(line);
let req = crate::command::parse_line(line);
let resp = router.dispatch(&req, &ctx);
stdout.write_all(resp.to_json_line().as_bytes())?;
@ -66,6 +66,6 @@ pub fn dispatch_command(
router: &CommandRouter,
ctx: &CommandContext,
) -> String {
let req = swactor_command::from_query_params(params);
let req = crate::command::from_query_params(params);
router.dispatch(&req, ctx).to_json_line()
}

View file

@ -1,4 +1,5 @@
pub mod collector;
pub mod command;
pub mod history;
pub mod investigate;
pub mod layer;

View file

@ -132,7 +132,7 @@ pub(crate) fn spawn_http_server(
let addr = format!("0.0.0.0:{port}");
let server = tiny_http::Server::http(&addr).expect("failed to bind HTTP server");
let server = Arc::new(server);
let cmd_router = Arc::new(swactor_command::CommandRouter::with_builtins());
let cmd_router = Arc::new(crate::command::CommandRouter::with_builtins());
for _ in 0..4 {
let server = Arc::clone(&server);
@ -362,7 +362,7 @@ fn handle_investigate_api(
url: &str,
runtime: Arc<Mutex<Option<Arc<Runtime>>>>,
collector: Arc<Mutex<Option<Arc<StatsCollector>>>>,
cmd_router: Arc<swactor_command::CommandRouter>,
cmd_router: Arc<crate::command::CommandRouter>,
) {
let params = parse_query_string(url);
@ -371,13 +371,13 @@ fn handle_investigate_api(
let json = match (maybe_rt, maybe_col) {
(Some(rt), Some(col)) => {
let ctx = swactor_command::CommandContext::with_enricher(rt, col);
let req = swactor_command::from_query_params(&params);
let ctx = crate::command::CommandContext::with_enricher(rt, col);
let req = crate::command::from_query_params(&params);
cmd_router.dispatch(&req, &ctx).to_json_line()
}
(Some(rt), None) => {
let ctx = swactor_command::CommandContext::new(rt);
let req = swactor_command::from_query_params(&params);
let ctx = crate::command::CommandContext::new(rt);
let req = crate::command::from_query_params(&params);
cmd_router.dispatch(&req, &ctx).to_json_line()
}
_ => {

View file

@ -7,7 +7,7 @@ use std::sync::Arc;
use swactor::actor::ActorInterface;
use swactor::runtime::{Ctx, Runtime, RuntimeConfig};
use swactor_command::{
use runtime_dashboard::command::{
from_query_params, parse_line, CommandContext, CommandRequest, CommandResponse, CommandRouter,
};
@ -310,9 +310,9 @@ fn from_query_params_defaults_to_help() {
#[test]
fn custom_command_handler() {
struct PingCommand;
impl swactor_command::CommandHandler for PingCommand {
fn meta(&self) -> swactor_command::CommandMeta {
swactor_command::CommandMeta {
impl runtime_dashboard::command::CommandHandler for PingCommand {
fn meta(&self) -> runtime_dashboard::command::CommandMeta {
runtime_dashboard::command::CommandMeta {
name: "ping",
description: "Respond with pong",
usage: "ping",

View file

@ -13,6 +13,7 @@ fn make_event(message: &str) -> DashboardEvent {
level: "INFO".into(),
message: message.into(),
worker_id: None,
actor_addr: None,
fields: serde_json::Map::new(),
}
}
@ -181,6 +182,7 @@ fn three_workers_report_independently_merged_view_is_complete() {
last_msg_type: Some("Ping"),
messages_processed: 100,
poisoned: false,
message_type_counts: vec![],
}]);
// Worker 1 reports 1 actor
@ -190,6 +192,7 @@ fn three_workers_report_independently_merged_view_is_complete() {
last_msg_type: None,
messages_processed: 50,
poisoned: false,
message_type_counts: vec![],
}]);
// Worker 2 reports 1 actor (poisoned)
@ -199,6 +202,7 @@ fn three_workers_report_independently_merged_view_is_complete() {
last_msg_type: Some("BadMsg"),
messages_processed: 10,
poisoned: true,
message_type_counts: vec![],
}]);
// Dashboard reads merged view
@ -228,6 +232,7 @@ fn worker_update_replaces_stale_snapshot() {
last_msg_type: None,
messages_processed: 10,
poisoned: false,
message_type_counts: vec![],
}]);
assert_eq!(collector.actor_details().len(), 1);
@ -240,6 +245,7 @@ fn worker_update_replaces_stale_snapshot() {
last_msg_type: Some("Update"),
messages_processed: 25,
poisoned: false,
message_type_counts: vec![],
}]);
let details = collector.actor_details();
@ -261,6 +267,7 @@ fn worker_reports_empty_after_all_actors_stop() {
last_msg_type: None,
messages_processed: 5,
poisoned: false,
message_type_counts: vec![],
}]);
assert_eq!(collector.actor_details().len(), 1);

View file

@ -1,11 +0,0 @@
[package]
name = "simulation-dashboard"
version = "0.1.0"
edition = "2024"
[dependencies]
simulation = { path = "../simulation", features = ["gossip"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tiny_http = "0.12"
toml = "0.8"

View file

@ -1,126 +0,0 @@
# gossip-dashboard
Interactive web dashboard for visualizing gossip protocol simulations.
The workflow has two steps:
1. **Generate traces** -- run simulations against TOML config files, producing `.trace.json` files.
2. **Replay traces** -- start the dashboard server, point it at a directory of traces, and explore them in the browser.
## Quick start
```bash
# 1. Generate traces from the bundled configs (outputs to traces/)
cargo run -p gossip-dashboard --example generate_traces
# 2. Launch the dashboard
cargo run -p gossip-dashboard --example replay -- traces
# => open http://localhost:8080
```
## Commands
### `generate_traces`
Runs gossip simulations and writes `.trace.json` files.
```
generate_traces # all bundled configs -> traces/
generate_traces <out-dir> # all bundled configs -> <out-dir>/
generate_traces <out-dir> <config.toml> [more.toml …] # specific configs -> <out-dir>/
```
Bundled configs live in `examples/configs/`. When no config paths are given, every `.toml` in that directory is run.
Output filenames are derived from the simulation name (lowercased, spaces to underscores). Example output:
```
traces/
ring_10_nodes.trace.json
star_7_nodes.trace.json
chain_8_nodes.trace.json
full_mesh_6_nodes.trace.json
partition_&_heal_8_nodes.trace.json
```
### `replay`
Starts an HTTP server that serves the dashboard UI and the trace data.
```
replay <trace-dir> [port]
```
| Argument | Required | Default | Description |
|-------------|----------|---------|------------------------------------------|
| `trace-dir` | yes | -- | Directory containing `.trace.json` files |
| `port` | no | 8080 | Port to bind on |
The server exposes three endpoints:
| Route | Description |
|--------------------------|------------------------------------|
| `GET /` | Dashboard HTML |
| `GET /traces` | JSON list of available trace files |
| `GET /trace.json?file=…` | Fetch a specific trace |
## Configuration (TOML)
Each simulation is defined by a TOML file. Example (`ring_10.toml`):
```toml
name = "Ring (10 nodes)"
topology = "ring"
num_nodes = 10
num_rounds = 15
ticks_per_round = 5
num_threads = 1
[initial_data]
color = "blue"
version = "1"
status = "active"
```
### Fields
| Field | Type | Required | Description |
|--------------------|-------------------|----------|-------------------------------------------------------------------|
| `name` | string | yes | Display name for the simulation |
| `topology` | string | yes | Network topology (see below) |
| `num_nodes` | integer | yes | Number of gossip nodes |
| `num_rounds` | integer | yes | Number of gossip rounds to run |
| `ticks_per_round` | integer | yes | Simulation ticks per round |
| `num_threads` | integer | yes | Worker threads (`1` = deterministic single-threaded) |
| `heal_after_round` | integer | no | Round after which partitioned halves are bridged |
| `initial_data` | table of strings | no | Key-value pairs seeded on node 0 before gossip begins |
### Topologies
| Value | Shape |
|---------------|--------------------------------------------------------------------------|
| `ring` | Each node connects to the next, forming a circle |
| `star` | Node 0 is a hub with bidirectional links to every other node |
| `full_mesh` | Every node connects bidirectionally to every other node |
| `chain` | Unidirectional chain: node 0 -> 1 -> 2 -> ... -> N-1 |
| `partitioned` | Two isolated full-mesh halves; use `heal_after_round` to bridge them |
## Bundled configs
| File | Topology | Nodes | Rounds | Notes |
|---------------------------|-------------|-------|--------|----------------------------|
| `ring_10.toml` | ring | 10 | 15 | |
| `star_7.toml` | star | 7 | 10 | |
| `full_mesh_6.toml` | full_mesh | 6 | 8 | |
| `chain_8.toml` | chain | 8 | 20 | |
| `partitioned_8_heal.toml` | partitioned | 8 | 20 | Heals after round 10 |
## Dashboard UI
Once a trace is loaded in the browser:
- **Graph canvas** -- nodes arranged in a circle then refined with force-directed layout. Nodes and edges flash as events are replayed.
- **Stats panel** -- total nodes, edges, messages, current round.
- **Worker logs** -- per-thread activity feed.
- **Event table** -- full event log with columns: Seq, Round, Thread, Node, Event, Details.
- **Playback controls** -- First / Prev / Play / Pause / Next / Last, timeline slider, speed adjustment (10 ms -- 2000 ms per event).

View file

@ -6,6 +6,7 @@ edition = "2024"
[features]
default = []
gossip = ["dep:log"]
dashboard = ["gossip", "dep:tiny_http", "dep:toml"]
[dependencies]
distribution = { path = "../distribution" }
@ -14,6 +15,8 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
getrandom = "0.2"
log = { version = "0.4", optional = true }
tiny_http = { version = "0.12", optional = true }
toml = { version = "0.8", optional = true }
[dev-dependencies]
simulation = { path = ".", features = ["gossip"] }
@ -21,3 +24,11 @@ simulation = { path = ".", features = ["gossip"] }
[[example]]
name = "gossip_sim"
required-features = ["gossip"]
[[example]]
name = "replay"
required-features = ["dashboard"]
[[example]]
name = "generate_traces"
required-features = ["dashboard"]

View file

@ -1,7 +1,7 @@
use std::path::PathBuf;
use simulation_dashboard::config::SimFileConfig;
use simulation_dashboard::save_trace;
use simulation::dashboard::config::SimFileConfig;
use simulation::dashboard::save_trace;
use simulation::gossip::sim::run_simulation;
const CONFIGS_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/configs");
@ -54,7 +54,7 @@ fn main() {
trace.events.len()
);
}
eprintln!("Done. View with: cargo run -p simulation-dashboard --example replay -- {out_dir}");
eprintln!("Done. View with: cargo run -p simulation --features dashboard --example replay -- {out_dir}");
}
fn collect_configs(dir: &str) -> Vec<PathBuf> {

View file

@ -1,4 +1,4 @@
use simulation_dashboard::serve_dashboard;
use simulation::dashboard::serve_dashboard;
fn main() {
let args: Vec<String> = std::env::args().collect();

View file

@ -3,8 +3,8 @@ use std::fs;
use std::io;
use serde::Deserialize;
use simulation::gossip::sim::GossipSimConfig;
use simulation::topology::Topology;
use crate::gossip::sim::GossipSimConfig;
use crate::topology::Topology;
#[derive(Deserialize)]
pub struct SimFileConfig {

View file

@ -7,7 +7,7 @@ pub use server::serve_dashboard;
use std::fs;
use std::io;
use simulation::gossip::trace::SimulationTrace;
use crate::gossip::trace::SimulationTrace;
pub fn save_trace(trace: &SimulationTrace, path: &str) -> io::Result<()> {
let json = serde_json::to_string_pretty(trace)

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::dashboard_html::DASHBOARD_HTML;
use super::dashboard_html::DASHBOARD_HTML;
// ── Trace directory scanning ──────────────────────────────────────────

View file

@ -6,3 +6,6 @@ pub mod distribution;
#[cfg(feature = "gossip")]
pub mod gossip;
#[cfg(feature = "dashboard")]
pub mod dashboard;

View file

@ -4,7 +4,7 @@ use wasm_bindgen::prelude::*;
use swactor::actor::{ActorAddress, ActorExited, ActorInterface};
use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeConfig};
use swactor_std::{CtxGroups, RuntimeNaming, RuntimeGroups, StdExtension};
use swactor_std::{CtxGroups, CtxWatching, RuntimeNaming, RuntimeGroups, StdExtension};
// ─── Core JS-facing types ───────────────────────────────────────────────────

View file

@ -1,6 +1,6 @@
# Wasm Actor Crate — Development History
> Adds a new crate (`crates/wasm-actor/`) that runs WebAssembly guest code
> Adds a new crate (`crates/bin-runner/`) that runs WebAssembly guest code
> **inside** a swactor actor. The Wasm instance lives in the actor — not as a
> separate OS process. Messages arrive as bytes, get written into Wasm linear
> memory, and the guest's `handle` export is called.
@ -30,7 +30,7 @@ Swactor already supported running *inside* a browser via `crates/wasm/`
plugins, multi-language actors, and capability-restricted compute.
The main swactor crate has no wasmtime dependency — all Wasm machinery is
isolated in `crates/wasm-actor/`.
isolated in `crates/bin-runner/`.
---
@ -38,14 +38,14 @@ isolated in `crates/wasm-actor/`.
| Component | Location | Purpose |
|-----------|----------|---------|
| `swactor-wasm-actor` crate | `crates/wasm-actor/` | Host-side: engine, builder, actor impl |
| 3 guest crates | `crates/wasm-actor/tests/guests/{echo,double,silent}/` | `#![no_std]` Wasm modules for testing |
| Integration tests | `crates/wasm-actor/tests/wasm_actor.rs` | 7 behavioral tests |
| `swactor-bin-runner` crate | `crates/bin-runner/` | Host-side: engine, builder, actor impl |
| 3 guest crates | `crates/bin-runner/tests/guests/{echo,double,silent}/` | `#![no_std]` Wasm modules for testing |
| Integration tests | `crates/bin-runner/tests/wasm_actor.rs` | 7 behavioral tests |
### Crate modules
```
crates/wasm-actor/src/
crates/bin-runner/src/
lib.rs — ByteMessage, re-exports
engine.rs — SharedEngine (Arc<wasmtime::Engine>)
builder.rs — WasmActorBuilder (compile + link + instantiate)
@ -138,9 +138,9 @@ This allows guests to send replies without hardcoding addresses.
```bash
rustup target add wasm32-unknown-unknown # one-time
cd crates/wasm-actor/tests/guests/echo && cargo build --target wasm32-unknown-unknown --release
cd crates/wasm-actor/tests/guests/double && cargo build --target wasm32-unknown-unknown --release
cd crates/wasm-actor/tests/guests/silent && cargo build --target wasm32-unknown-unknown --release
cd crates/bin-runner/tests/guests/echo && cargo build --target wasm32-unknown-unknown --release
cd crates/bin-runner/tests/guests/double && cargo build --target wasm32-unknown-unknown --release
cd crates/bin-runner/tests/guests/silent && cargo build --target wasm32-unknown-unknown --release
```
Each guest crate has its own `[workspace]` marker to stay independent of the
@ -178,7 +178,7 @@ root workspace.
## 8. Test Coverage Summary
7 behavioral tests in `crates/wasm-actor/tests/wasm_actor.rs`:
7 behavioral tests in `crates/bin-runner/tests/wasm_actor.rs`:
| Test | Scenario |
|------|----------|

View file

@ -10,7 +10,7 @@ dependencies = [
]
[tool.uv.sources]
swactor = { path = "../python", editable = true }
swactor = { path = "../../crates/python", editable = true }
[[tool.uv.index]]
name = "pytorch-cpu"

View file

@ -16,4 +16,4 @@ SWACTOR_TRACE_DIR="$DIR" cargo test -p simulation \
COUNT=$(find "$DIR" -name '*.trace.json' 2>/dev/null | wc -l)
echo "$COUNT traces in $DIR/"
echo "Dashboard at http://localhost:$PORT"
cargo run -p simulation-dashboard --example replay -- "$DIR" "$PORT"
cargo run -p simulation --features dashboard --example replay -- "$DIR" "$PORT"