diff --git a/Cargo.lock b/Cargo.lock index e0456fc..38855b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1939,7 +1939,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "picard" version = "0.4.2" -source = "git+https://github.com/picard-rs/picard?rev=50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5#50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5" +source = "git+https://github.com/picard-rs/picard?rev=6186d05b46b432cecf1ca1e0686088de08ef5640#6186d05b46b432cecf1ca1e0686088de08ef5640" dependencies = [ "better_default", "chrono", @@ -1956,6 +1956,7 @@ dependencies = [ "slog-scope", "slog-stdlog", "thiserror 2.0.21", + "tracing", "uuid", "wasm-bindgen-futures", "web-time", @@ -1964,7 +1965,7 @@ dependencies = [ [[package]] name = "picard-macros" version = "0.2.0" -source = "git+https://github.com/picard-rs/picard?rev=50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5#50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5" +source = "git+https://github.com/picard-rs/picard?rev=6186d05b46b432cecf1ca1e0686088de08ef5640#6186d05b46b432cecf1ca1e0686088de08ef5640" dependencies = [ "proc-macro2", "quote", @@ -2802,6 +2803,7 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "tracing-subscriber", "wasm-bindgen-cli-support", ] diff --git a/Cargo.toml b/Cargo.toml index d95d28e..3bac44e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,11 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "any", "sqlite", "postgres"] } rand = "0.8" -picard = { git = "https://github.com/picard-rs/picard", rev = "50ec5b75006efe422b2bd7a92bc6b4eaf3e97ae5" } +picard = { git = "https://github.com/picard-rs/picard", rev = "6186d05b46b432cecf1ca1e0686088de08ef5640", features = [ + "stats", + "tracing", +] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } [build-dependencies] wasm-bindgen-cli-support = "=0.2.129" diff --git a/README.md b/README.md index ae41ddb..99d7bf9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A browser-based world simulation with a Rust server and a Leptos/Wasm UI. The server runs the simulation and exposes its state over HTTP and WebSockets. You can edit terrain and resources, manage people and animals, and customize agent behavior with the included Rune script. -Living simulation entities (people, rabbits, and wolves) run as Picard actors. The world coordinates terrain, shared resources, and actor messages; Rune behavior scripts remain on the simulation thread because Rune's VM is not `Send`. +Living simulation entities (people, rabbits, and wolves) run as Picard actors. The world coordinates terrain, shared resources, and actor messages; Rune behavior scripts remain on the simulation thread because Rune's VM is not `Send`. Picard's tracing and runtime statistics features are enabled: actor lifecycle events are logged (configure verbosity with `RUST_LOG`, for example `RUST_LOG=picard=trace`), and cumulative actor/message counters are shown in the UI. The **Calendar and life-stage durations** panel configures ticks per day and the number of days, weeks, and months in larger calendar units. Each human life stage has an inclusive minimum/maximum duration in in-game days; the world samples within that range when a person enters the stage. These settings are stored with the world snapshot. diff --git a/src/main.rs b/src/main.rs index 766e880..960efb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -236,6 +236,13 @@ async fn client(mut socket: WebSocket, app: App) { #[tokio::main] async fn main() { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("picard=info")); + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_target(false) + .init(); + let seed = std::env::args() .nth(1) .and_then(|s| s.parse().ok()) diff --git a/src/world.rs b/src/world.rs index 445ff47..cc3dc33 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1836,6 +1836,7 @@ impl World { let week = (day / self.calendar.days_per_week as u64) % self.calendar.weeks_per_month as u64 + 1; let date_day = day % self.calendar.days_per_week as u64 + 1; + let actor_stats = self.actor_system.stats(); let _ = write!( s, "{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"year\":{},\"month\":{},\"week\":{},\"date_day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],", @@ -1947,7 +1948,17 @@ impl World { s.push_str("],\"log\":["); let j: Vec = self.log.iter().map(|l| format!("\"{}\"", l)).collect(); s.push_str(&j.join(",")); - s.push_str("]}"); + let _ = write!( + s, + "],\"actor_stats\":{{\"actors_created\":{},\"actors_terminated\":{},\"actors_restarted\":{},\"messages_sent\":{},\"messages_failed\":{},\"system_messages_sent\":{},\"live_actors\":{}}}}}", + actor_stats.actors_created, + actor_stats.actors_terminated, + actor_stats.actors_restarted, + actor_stats.messages_sent, + actor_stats.messages_failed, + actor_stats.system_messages_sent, + actor_stats.live_actors() + ); s } } @@ -1962,6 +1973,12 @@ impl Drop for World { mod tests { use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, H, LifeStage, W, World, cheb}; + fn simulation_state(world: &World) -> serde_json::Value { + let mut state: serde_json::Value = serde_json::from_str(&world.state_json()).unwrap(); + state.as_object_mut().unwrap().remove("actor_stats"); + state + } + #[test] fn new_people_progress_through_every_stage_and_die() { let mut world = World::new(42); @@ -2150,7 +2167,7 @@ mod tests { let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap(); assert_eq!(restored.partnerships.len(), 4); - assert_eq!(restored.state_json(), world.state_json()); + assert_eq!(simulation_state(&restored), simulation_state(&world)); assert_eq!( serde_json::from_str::(&restored.state_json()).unwrap()["people"][0] [8], @@ -2197,11 +2214,11 @@ mod tests { let snapshot = original.snapshot_json().unwrap(); let mut restored = World::from_snapshot(&snapshot, 42).unwrap(); - assert_eq!(original.state_json(), restored.state_json()); + assert_eq!(simulation_state(&original), simulation_state(&restored)); original.step(); restored.step(); - assert_eq!(original.state_json(), restored.state_json()); + assert_eq!(simulation_state(&original), simulation_state(&restored)); } #[test] @@ -2240,6 +2257,11 @@ mod tests { world.entities.len(), world.people.len() + world.rabbits.len() + world.wolves.len() ); + let state: serde_json::Value = serde_json::from_str(&world.state_json()).unwrap(); + assert!( + state["actor_stats"]["actors_created"].as_u64().unwrap() >= world.entities.len() as u64 + ); + assert!(state["actor_stats"]["messages_sent"].as_u64().unwrap() > 0); } #[test] @@ -2255,7 +2277,7 @@ mod tests { assert_eq!(restored.calendar.days_per_week, 5); assert_eq!(restored.calendar.toddler.min_days, 3); assert_eq!(restored.calendar.toddler.max_days, 9); - assert_eq!(world.state_json(), restored.state_json()); + assert_eq!(simulation_state(&world), simulation_state(&restored)); } #[test] diff --git a/ui/src/lib.rs b/ui/src/lib.rs index c5589b4..b821ff3 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -164,6 +164,19 @@ fn App() -> impl IntoView { "Stockpile: food {}, wood {}, stone {}, tools {}", s.stock.food, s.stock.wood, s.stock.stone, s.stock.tools )} +
{format!( + "Actors: {} live ({} created, {} stopped, {} restarts)", + s.actor_stats.live_actors, + s.actor_stats.actors_created, + s.actor_stats.actors_terminated, + s.actor_stats.actors_restarted + )}
+
{format!( + "Messages: {} sent, {} failed, {} system", + s.actor_stats.messages_sent, + s.actor_stats.messages_failed, + s.actor_stats.system_messages_sent + )}
} }))}
diff --git a/ui/src/model.rs b/ui/src/model.rs index a72fc35..a47cf92 100644 --- a/ui/src/model.rs +++ b/ui/src/model.rs @@ -20,6 +20,17 @@ pub struct Stock { pub tools: u32, } +#[derive(Deserialize, Clone, Default)] +pub struct ActorRuntimeStats { + pub actors_created: u64, + pub actors_terminated: u64, + pub actors_restarted: u64, + pub messages_sent: u64, + pub messages_failed: u64, + pub system_messages_sent: u64, + pub live_actors: u64, +} + #[derive(Deserialize, Clone)] pub struct StageRange { pub min_days: u32, @@ -138,6 +149,7 @@ pub struct State { pub hour: f32, pub daylight: f32, pub stock: Stock, + pub actor_stats: ActorRuntimeStats, pub home: (usize, usize), pub houses: Vec<(usize, usize)>, pub people: Vec,