use std::fmt;
use std::time::{Duration, Instant};
use crate::base::arena::Arena;
use crate::base::node::ExprId;
pub(crate) const EXPR_CHARS: usize = 200;
pub(crate) const HOT_MS: u64 = 250;
pub(crate) const HOT_NODES: usize = 200_000;
macro_rules! stage {
($arena:ident, $name:literal, $expr:expr, $body:expr) => {{
let span = ::tracing::debug_span!(target: "symplex::stage", $name);
let guard = if span.is_disabled() {
None
} else {
Some($crate::base::stage::Stage::enter(
span, $name, &$arena, $expr,
))
};
let out = $body;
if let Some(guard) = guard {
guard.exit($arena.node_count());
}
out
}};
}
pub(crate) use stage;
pub(crate) struct Stage {
_span: tracing::span::EnteredSpan,
name: &'static str,
start: Instant,
nodes: usize,
}
impl Stage {
pub(crate) fn enter(
span: tracing::Span,
name: &'static str,
arena: &Arena,
expr: ExprId,
) -> Self {
let span = span.entered();
let nodes = arena.node_count();
tracing::trace!(
target: "symplex::stage",
nodes,
expr = %Clipped(arena.display(expr)),
"enter"
);
Stage {
_span: span,
name,
start: Instant::now(),
nodes,
}
}
pub(crate) fn exit(self, nodes_now: usize) {
let elapsed = self.start.elapsed();
let new_nodes = nodes_now.saturating_sub(self.nodes);
if elapsed >= Duration::from_millis(HOT_MS) || new_nodes >= HOT_NODES {
tracing::debug!(
target: "symplex::stage",
stage = self.name,
ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX),
new_nodes,
"hot stage"
);
} else {
tracing::trace!(
target: "symplex::stage",
us = u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX),
new_nodes,
"exit"
);
}
}
}
struct Clipped<D>(D);
impl<D: fmt::Display> fmt::Display for Clipped<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct Limit {
buf: String,
left: usize,
}
impl fmt::Write for Limit {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.chars() {
if self.left == 0 {
return Err(fmt::Error);
}
self.buf.push(c);
self.left -= 1;
}
Ok(())
}
}
let mut w = Limit {
buf: String::new(),
left: EXPR_CHARS,
};
let clipped = fmt::write(&mut w, format_args!("{}", self.0)).is_err();
f.write_str(&w.buf)?;
if clipped {
f.write_str("…")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{Clipped, EXPR_CHARS};
use crate::base::arena::Arena;
#[test]
fn stage_evaluates_to_its_body() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let y = stage!(arena, "test_stage", x, arena.int(7));
assert_eq!(arena.as_num(y).map(ToString::to_string), Some("7".into()));
}
#[test]
fn clipped_stops_at_the_limit() {
let long = "x".repeat(EXPR_CHARS + 50);
let shown = Clipped(&long).to_string();
assert_eq!(shown.chars().count(), EXPR_CHARS + 1);
assert!(shown.ends_with('…'));
assert_eq!(Clipped("short").to_string(), "short");
}
#[tracing_test::traced_test]
#[test]
fn enabled_stage_logs_enter_and_exit() {
let mut arena = Arena::new();
let x = arena.symbol("x");
let y = stage!(arena, "probe_stage", x, arena.int(3));
assert_eq!(arena.as_num(y).map(ToString::to_string), Some("3".into()));
assert!(logs_contain("probe_stage"));
assert!(logs_contain("enter"));
assert!(logs_contain("expr=x"));
assert!(logs_contain("new_nodes="));
}
}