Files
benilla-octo/crates/benilla-assets/src/trace.rs
T
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.

Today's work, which is the reason a baseline exists at all:

- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
  characters were entering the sun's shadow cascades, so every skinned-rig
  shadow specialization compiled live mid-frame; the hitch detector logged
  "main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
  the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
  tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
  an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
  and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
  measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
  frame-cost reading no longer pays 2.7 ms a frame for it.

Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 15:01:14 -06:00

77 lines
2.7 KiB
Rust

//! The shared debug-trace sink behind `WOW_MOVE_TRACE=<path>`: one file and one clock for every
//! layer's tagged lines, engine and game alike, so it sits below both. `in` and `out` are the full
//! inbound and outbound opcode streams; `out` is written after the send, where the mover's `snd`
//! is a decision before queueing. Each file opens with a `# t0=<unix epoch>` header so two
//! clients' traces align. `WOW_MOVE_TRACE_TAGS="move,snd,in"` keeps only those tags: every line is
//! an unbuffered write under one mutex on the main thread, so a busy tag skews every `t=`.
use std::fs::File;
use std::io::Write;
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
struct Sink {
out: File,
t0: Instant,
}
static SINK: OnceLock<Option<Mutex<Sink>>> = OnceLock::new();
fn sink() -> Option<&'static Mutex<Sink>> {
SINK.get_or_init(|| {
let path = std::env::var("WOW_MOVE_TRACE").ok()?;
let mut out = File::create(&path)
.map_err(|e| eprintln!("dbg-trace: cannot create {path}: {e}"))
.ok()?;
// Wall time is `t0 + t`, which aligns the traces of two processes.
let t0_wall = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0.0, |d| d.as_secs_f64());
let _ = writeln!(out, "# t0={t0_wall:.3} (unix epoch seconds at t=0)");
Some(Mutex::new(Sink {
out,
t0: Instant::now(),
}))
})
.as_ref()
}
/// The tag allow-list from `WOW_MOVE_TRACE_TAGS`; `None`, unset or empty, keeps everything.
static TAGS: OnceLock<Option<Vec<String>>> = OnceLock::new();
/// Whether `tag` survives the allow-list, checked before the mutex so a filtered tag takes no lock.
fn tag_allowed(tag: &str) -> bool {
TAGS.get_or_init(|| {
let raw = std::env::var("WOW_MOVE_TRACE_TAGS").ok()?;
let allow: Vec<String> = raw
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
(!allow.is_empty()).then_some(allow)
})
.as_ref()
.is_none_or(|allow| allow.iter().any(|t| t == tag))
}
/// Whether the trace is on, so a caller can skip building its line.
pub fn enabled() -> bool {
sink().is_some()
}
/// [`enabled`] plus the allow-list, for a caller whose line is expensive to build.
pub fn enabled_for(tag: &str) -> bool {
enabled() && tag_allowed(tag)
}
/// Append one tagged line, stamped with the sink's shared clock.
pub fn line(tag: &str, msg: &str) {
let Some(sink) = sink() else { return };
if !tag_allowed(tag) {
return;
}
let Ok(mut s) = sink.lock() else { return };
let t = s.t0.elapsed().as_secs_f32();
let _ = writeln!(s.out, "t={t:9.3} {tag:4} {msg}");
}