use std::collections::{BTreeMap, HashMap};
use std::fmt::Debug;
use std::mem;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::time::{Duration, Instant};
use bevy_ecs::observer::Observer;
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemParam;
use bevy_log::tracing::Subscriber;
use bevy_log::tracing::field::{Field, Visit};
use bevy_log::tracing::span::{Attributes, Id};
use bevy_log::tracing_subscriber::layer::{Context, Layer};
use bevy_log::tracing_subscriber::registry::LookupSpan;
use crate::{PluginSystems, ProvidedBy};
const SYSTEM: &str = "system";
const SCHEDULE: &str = "schedule";
#[derive(Resource, Clone, Default)]
pub struct SystemTimings(Arc<Mutex<Timings>>);
#[derive(Default)]
struct Timings {
totals: BTreeMap<(String, Option<String>), Timing>,
frame: Vec<(String, Option<String>, Duration)>,
last: Vec<(String, Option<String>, Duration)>,
schedules: Vec<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Timing {
pub runs: u64,
pub total: Duration,
pub max: Duration,
}
impl Timing {
fn add(&mut self, took: Duration) {
self.runs += 1;
self.total += took;
self.max = self.max.max(took);
}
}
impl SystemTimings {
fn lock(&self) -> MutexGuard<'_, Timings> {
self.0.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn systems(&self) -> BTreeMap<(String, Option<String>), Timing> {
self.lock().totals.clone()
}
pub fn clear(&self) {
let mut timings = self.lock();
timings.totals.clear();
timings.frame.clear();
timings.last.clear();
}
fn next_frame(&self) {
let mut timings = self.lock();
let frame = mem::take(&mut timings.frame);
timings.last = frame;
}
fn record(&self, system: &str, took: Duration) {
let mut timings = self.lock();
let schedule = timings.schedules.last().cloned();
let key = (system.to_owned(), schedule.clone());
timings.totals.entry(key).or_default().add(took);
timings.frame.push((system.to_owned(), schedule, took));
}
}
pub(crate) fn next_frame(timings: Res<SystemTimings>) {
timings.next_frame();
}
#[derive(SystemParam)]
pub struct PluginTimings<'w, 's> {
timings: Res<'w, SystemTimings>,
systems: Option<Res<'w, PluginSystems>>,
observers: Query<'w, 's, (&'static Observer, &'static ProvidedBy)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SystemRun {
pub system: String,
pub plugin: Option<Entity>,
pub schedule: Option<String>,
pub took: Duration,
}
impl PluginTimings<'_, '_> {
pub fn totals(&self) -> BTreeMap<(Option<Entity>, Option<String>), Timing> {
let plugin = self.plugin_of();
let mut totals = BTreeMap::<_, Timing>::new();
for ((system, schedule), timing) in self.timings.lock().totals.iter() {
let total = totals.entry((plugin(system, schedule), schedule.clone()));
let total = total.or_default();
total.runs += timing.runs;
total.total += timing.total;
total.max = total.max.max(timing.max);
}
totals
}
pub fn last_frame(&self) -> Vec<SystemRun> {
let plugin = self.plugin_of();
let last = self.timings.lock().last.clone();
let runs = last.into_iter().map(|(system, schedule, took)| SystemRun {
plugin: plugin(&system, &schedule),
system,
schedule,
took,
});
runs.collect()
}
fn plugin_of(&self) -> impl Fn(&str, &Option<String>) -> Option<Entity> + '_ {
let mut observers = HashMap::<String, Option<Entity>>::new();
for (observer, &ProvidedBy(plugin)) in &self.observers {
let by = observers.entry(observer.system_name().to_string());
let by = by.or_insert(Some(plugin));
if *by != Some(plugin) {
*by = None;
}
}
move |system, schedule| {
let added = schedule.as_ref().and_then(|schedule| {
let systems = self.systems.as_ref()?;
systems.plugin(system, schedule)
});
added.or_else(|| observers.get(system).copied().flatten())
}
}
}
pub(crate) struct TimeSystems(pub(crate) SystemTimings);
struct Running {
system: String,
since: Option<Instant>,
}
struct Scheduled(String);
#[derive(Default)]
struct SpanName(String);
impl Visit for SpanName {
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "name" {
value.clone_into(&mut self.0);
}
}
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
if field.name() == "name" && self.0.is_empty() {
self.0 = format!("{value:?}");
}
}
}
impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for TimeSystems {
fn on_new_span(&self, attributes: &Attributes<'_>, id: &Id, context: Context<'_, S>) {
let kind = attributes.metadata().name();
if kind != SYSTEM && kind != SCHEDULE {
return;
}
let mut name = SpanName::default();
attributes.record(&mut name);
let Some(span) = context.span(id) else {
return;
};
let mut extensions = span.extensions_mut();
if kind == SYSTEM {
extensions.insert(Running {
system: name.0,
since: None,
});
} else {
extensions.insert(Scheduled(name.0));
}
}
fn on_enter(&self, id: &Id, context: Context<'_, S>) {
let Some(span) = context.span(id) else {
return;
};
let mut extensions = span.extensions_mut();
if let Some(running) = extensions.get_mut::<Running>() {
running.since = Some(Instant::now());
} else if let Some(Scheduled(label)) = extensions.get_mut::<Scheduled>() {
self.0.lock().schedules.push(label.clone());
}
}
fn on_exit(&self, id: &Id, context: Context<'_, S>) {
let Some(span) = context.span(id) else {
return;
};
let mut extensions = span.extensions_mut();
if let Some(running) = extensions.get_mut::<Running>()
&& let Some(since) = running.since.take()
{
self.0.record(&running.system, since.elapsed());
} else if extensions.get_mut::<Scheduled>().is_some() {
self.0.lock().schedules.pop();
}
}
}
#[cfg(test)]
mod tests;