1use std::collections::{BTreeMap, HashMap};
31use std::fmt::Debug;
32use std::mem;
33use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
34use std::time::{Duration, Instant};
35
36use bevy_ecs::observer::Observer;
37use bevy_ecs::prelude::*;
38use bevy_ecs::system::SystemParam;
39use bevy_log::tracing::Subscriber;
40use bevy_log::tracing::field::{Field, Visit};
41use bevy_log::tracing::span::{Attributes, Id};
42use bevy_log::tracing_subscriber::layer::{Context, Layer};
43use bevy_log::tracing_subscriber::registry::LookupSpan;
44
45use crate::{PluginSystems, ProvidedBy};
46
47const SYSTEM: &str = "system";
49
50const SCHEDULE: &str = "schedule";
52
53#[derive(Resource, Clone, Default)]
58pub struct SystemTimings(Arc<Mutex<Timings>>);
59
60#[derive(Default)]
62struct Timings {
63 totals: BTreeMap<(String, Option<String>), Timing>,
65 frame: Vec<(String, Option<String>, Duration)>,
67 last: Vec<(String, Option<String>, Duration)>,
69 schedules: Vec<String>,
72}
73
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct Timing {
77 pub runs: u64,
79 pub total: Duration,
81 pub max: Duration,
83}
84
85impl Timing {
86 fn add(&mut self, took: Duration) {
87 self.runs += 1;
88 self.total += took;
89 self.max = self.max.max(took);
90 }
91}
92
93impl SystemTimings {
94 fn lock(&self) -> MutexGuard<'_, Timings> {
95 self.0.lock().unwrap_or_else(PoisonError::into_inner)
96 }
97
98 pub fn systems(&self) -> BTreeMap<(String, Option<String>), Timing> {
100 self.lock().totals.clone()
101 }
102
103 pub fn clear(&self) {
105 let mut timings = self.lock();
106 timings.totals.clear();
107 timings.frame.clear();
108 timings.last.clear();
109 }
110
111 fn next_frame(&self) {
113 let mut timings = self.lock();
114 let frame = mem::take(&mut timings.frame);
115 timings.last = frame;
116 }
117
118 fn record(&self, system: &str, took: Duration) {
119 let mut timings = self.lock();
120 let schedule = timings.schedules.last().cloned();
121 let key = (system.to_owned(), schedule.clone());
122 timings.totals.entry(key).or_default().add(took);
123 timings.frame.push((system.to_owned(), schedule, took));
124 }
125}
126
127pub(crate) fn next_frame(timings: Res<SystemTimings>) {
129 timings.next_frame();
130}
131
132#[derive(SystemParam)]
136pub struct PluginTimings<'w, 's> {
137 timings: Res<'w, SystemTimings>,
138 systems: Option<Res<'w, PluginSystems>>,
139 observers: Query<'w, 's, (&'static Observer, &'static ProvidedBy)>,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct SystemRun {
145 pub system: String,
147 pub plugin: Option<Entity>,
149 pub schedule: Option<String>,
151 pub took: Duration,
153}
154
155impl PluginTimings<'_, '_> {
156 pub fn totals(&self) -> BTreeMap<(Option<Entity>, Option<String>), Timing> {
158 let plugin = self.plugin_of();
159 let mut totals = BTreeMap::<_, Timing>::new();
160 for ((system, schedule), timing) in self.timings.lock().totals.iter() {
161 let total = totals.entry((plugin(system, schedule), schedule.clone()));
162 let total = total.or_default();
163 total.runs += timing.runs;
164 total.total += timing.total;
165 total.max = total.max.max(timing.max);
166 }
167 totals
168 }
169
170 pub fn last_frame(&self) -> Vec<SystemRun> {
172 let plugin = self.plugin_of();
173 let last = self.timings.lock().last.clone();
174 let runs = last.into_iter().map(|(system, schedule, took)| SystemRun {
175 plugin: plugin(&system, &schedule),
176 system,
177 schedule,
178 took,
179 });
180 runs.collect()
181 }
182
183 fn plugin_of(&self) -> impl Fn(&str, &Option<String>) -> Option<Entity> + '_ {
185 let mut observers = HashMap::<String, Option<Entity>>::new();
186 for (observer, &ProvidedBy(plugin)) in &self.observers {
187 let by = observers.entry(observer.system_name().to_string());
188 let by = by.or_insert(Some(plugin));
189 if *by != Some(plugin) {
190 *by = None;
191 }
192 }
193 move |system, schedule| {
194 let added = schedule.as_ref().and_then(|schedule| {
195 let systems = self.systems.as_ref()?;
196 systems.plugin(system, schedule)
197 });
198 added.or_else(|| observers.get(system).copied().flatten())
199 }
200 }
201}
202
203pub(crate) struct TimeSystems(pub(crate) SystemTimings);
206
207struct Running {
209 system: String,
210 since: Option<Instant>,
211}
212
213struct Scheduled(String);
215
216#[derive(Default)]
218struct SpanName(String);
219
220impl Visit for SpanName {
221 fn record_str(&mut self, field: &Field, value: &str) {
222 if field.name() == "name" {
223 value.clone_into(&mut self.0);
224 }
225 }
226
227 fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
228 if field.name() == "name" && self.0.is_empty() {
229 self.0 = format!("{value:?}");
230 }
231 }
232}
233
234impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for TimeSystems {
235 fn on_new_span(&self, attributes: &Attributes<'_>, id: &Id, context: Context<'_, S>) {
236 let kind = attributes.metadata().name();
237 if kind != SYSTEM && kind != SCHEDULE {
238 return;
239 }
240 let mut name = SpanName::default();
241 attributes.record(&mut name);
242 let Some(span) = context.span(id) else {
243 return;
244 };
245 let mut extensions = span.extensions_mut();
246 if kind == SYSTEM {
247 extensions.insert(Running {
248 system: name.0,
249 since: None,
250 });
251 } else {
252 extensions.insert(Scheduled(name.0));
253 }
254 }
255
256 fn on_enter(&self, id: &Id, context: Context<'_, S>) {
257 let Some(span) = context.span(id) else {
258 return;
259 };
260 let mut extensions = span.extensions_mut();
261 if let Some(running) = extensions.get_mut::<Running>() {
262 running.since = Some(Instant::now());
263 } else if let Some(Scheduled(label)) = extensions.get_mut::<Scheduled>() {
264 self.0.lock().schedules.push(label.clone());
265 }
266 }
267
268 fn on_exit(&self, id: &Id, context: Context<'_, S>) {
269 let Some(span) = context.span(id) else {
270 return;
271 };
272 let mut extensions = span.extensions_mut();
273 if let Some(running) = extensions.get_mut::<Running>()
274 && let Some(since) = running.since.take()
275 {
276 self.0.record(&running.system, since.elapsed());
277 } else if extensions.get_mut::<Scheduled>().is_some() {
278 self.0.lock().schedules.pop();
279 }
280 }
281}
282
283#[cfg(test)]
284mod tests;