feat(sim): expose Picard runtime stats and tracing

Enable Picard's stats and tracing features, publish actor counters to the UI, and initialize configurable tracing output.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Biedermann 2026-10-11 18:58:07 +02:00
parent a4eb181154
commit b6bab90503
7 changed files with 69 additions and 9 deletions

6
Cargo.lock generated
View File

@ -1939,7 +1939,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "picard" name = "picard"
version = "0.4.2" 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 = [ dependencies = [
"better_default", "better_default",
"chrono", "chrono",
@ -1956,6 +1956,7 @@ dependencies = [
"slog-scope", "slog-scope",
"slog-stdlog", "slog-stdlog",
"thiserror 2.0.21", "thiserror 2.0.21",
"tracing",
"uuid", "uuid",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-time", "web-time",
@ -1964,7 +1965,7 @@ dependencies = [
[[package]] [[package]]
name = "picard-macros" name = "picard-macros"
version = "0.2.0" 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 = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2802,6 +2803,7 @@ dependencies = [
"serde_json", "serde_json",
"sqlx", "sqlx",
"tokio", "tokio",
"tracing-subscriber",
"wasm-bindgen-cli-support", "wasm-bindgen-cli-support",
] ]

View File

@ -15,7 +15,11 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "any", "sqlite", "postgres"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "any", "sqlite", "postgres"] }
rand = "0.8" 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] [build-dependencies]
wasm-bindgen-cli-support = "=0.2.129" wasm-bindgen-cli-support = "=0.2.129"

View File

@ -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. 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. 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.

View File

@ -236,6 +236,13 @@ async fn client(mut socket: WebSocket, app: App) {
#[tokio::main] #[tokio::main]
async fn 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() let seed = std::env::args()
.nth(1) .nth(1)
.and_then(|s| s.parse().ok()) .and_then(|s| s.parse().ok())

View File

@ -1836,6 +1836,7 @@ impl World {
let week = let week =
(day / self.calendar.days_per_week as u64) % self.calendar.weeks_per_month as u64 + 1; (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 date_day = day % self.calendar.days_per_week as u64 + 1;
let actor_stats = self.actor_system.stats();
let _ = write!( let _ = write!(
s, s,
"{{\"paused\":{},\"speed\":{},\"seed\":{},\"rev\":{},\"tick\":{},\"day\":{},\"year\":{},\"month\":{},\"week\":{},\"date_day\":{},\"hour\":{:.2},\"daylight\":{:.3},\"stock\":{{\"food\":{},\"wood\":{},\"stone\":{},\"tools\":{}}},\"home\":[{},{}],", "{{\"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\":["); s.push_str("],\"log\":[");
let j: Vec<String> = self.log.iter().map(|l| format!("\"{}\"", l)).collect(); let j: Vec<String> = self.log.iter().map(|l| format!("\"{}\"", l)).collect();
s.push_str(&j.join(",")); 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 s
} }
} }
@ -1962,6 +1973,12 @@ impl Drop for World {
mod tests { mod tests {
use super::{DEFAULT_SCRIPT, DEFAULT_TICKS_PER_DAY, H, LifeStage, W, World, cheb}; 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] #[test]
fn new_people_progress_through_every_stage_and_die() { fn new_people_progress_through_every_stage_and_die() {
let mut world = World::new(42); let mut world = World::new(42);
@ -2150,7 +2167,7 @@ mod tests {
let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap(); let restored = World::from_snapshot(&world.snapshot_json().unwrap(), 42).unwrap();
assert_eq!(restored.partnerships.len(), 4); assert_eq!(restored.partnerships.len(), 4);
assert_eq!(restored.state_json(), world.state_json()); assert_eq!(simulation_state(&restored), simulation_state(&world));
assert_eq!( assert_eq!(
serde_json::from_str::<serde_json::Value>(&restored.state_json()).unwrap()["people"][0] serde_json::from_str::<serde_json::Value>(&restored.state_json()).unwrap()["people"][0]
[8], [8],
@ -2197,11 +2214,11 @@ mod tests {
let snapshot = original.snapshot_json().unwrap(); let snapshot = original.snapshot_json().unwrap();
let mut restored = World::from_snapshot(&snapshot, 42).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(); original.step();
restored.step(); restored.step();
assert_eq!(original.state_json(), restored.state_json()); assert_eq!(simulation_state(&original), simulation_state(&restored));
} }
#[test] #[test]
@ -2240,6 +2257,11 @@ mod tests {
world.entities.len(), world.entities.len(),
world.people.len() + world.rabbits.len() + world.wolves.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] #[test]
@ -2255,7 +2277,7 @@ mod tests {
assert_eq!(restored.calendar.days_per_week, 5); assert_eq!(restored.calendar.days_per_week, 5);
assert_eq!(restored.calendar.toddler.min_days, 3); assert_eq!(restored.calendar.toddler.min_days, 3);
assert_eq!(restored.calendar.toddler.max_days, 9); 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] #[test]

View File

@ -164,6 +164,19 @@ fn App() -> impl IntoView {
"Stockpile: food {}, wood {}, stone {}, tools {}", "Stockpile: food {}, wood {}, stone {}, tools {}",
s.stock.food, s.stock.wood, s.stock.stone, s.stock.tools s.stock.food, s.stock.wood, s.stock.stone, s.stock.tools
)}</div> )}</div>
<div>{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
)}</div>
<div>{format!(
"Messages: {} sent, {} failed, {} system",
s.actor_stats.messages_sent,
s.actor_stats.messages_failed,
s.actor_stats.system_messages_sent
)}</div>
} }
}))} }))}
<div> <div>

View File

@ -20,6 +20,17 @@ pub struct Stock {
pub tools: u32, 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)] #[derive(Deserialize, Clone)]
pub struct StageRange { pub struct StageRange {
pub min_days: u32, pub min_days: u32,
@ -138,6 +149,7 @@ pub struct State {
pub hour: f32, pub hour: f32,
pub daylight: f32, pub daylight: f32,
pub stock: Stock, pub stock: Stock,
pub actor_stats: ActorRuntimeStats,
pub home: (usize, usize), pub home: (usize, usize),
pub houses: Vec<(usize, usize)>, pub houses: Vec<(usize, usize)>,
pub people: Vec<Person>, pub people: Vec<Person>,