use bevy::prelude::*;
use std::time::Duration;
use crate::event::ReactEvents;
use crate::react_event;
use crate::reconcile::OpApplyStats;
use super::DevtoolsState;
#[react_event(name = "devtools.batchStats")]
struct DevtoolsBatchStats {
applied_count: u64,
last_ops: usize,
frame_wait_ms: f64,
pre_apply_ms: f64,
translate_ms: f64,
command_ms: f64,
layout_ms: f64,
}
#[derive(Resource, Default)]
pub(super) struct DevtoolsTimers {
#[cfg(not(target_arch = "wasm32"))]
pre_layout: Option<std::time::Instant>,
last_command: Duration,
last_layout: Duration,
seen_applied: u64,
}
#[cfg_attr(target_arch = "wasm32", allow(unused_mut, unused_variables))]
pub(super) fn mark_pre_layout(mut timers: ResMut<DevtoolsTimers>) {
#[cfg(not(target_arch = "wasm32"))]
{
timers.pre_layout = Some(std::time::Instant::now());
}
}
pub(super) fn mark_post_layout(stats: Res<OpApplyStats>, mut timers: ResMut<DevtoolsTimers>) {
if stats.applied_count == timers.seen_applied {
return;
}
timers.seen_applied = stats.applied_count;
#[cfg(not(target_arch = "wasm32"))]
if let (Some(end), Some(pre)) = (stats.last_apply_end, timers.pre_layout) {
let (command, layout) = split_legs(end, pre, std::time::Instant::now());
timers.last_command = command;
timers.last_layout = layout;
}
}
#[cfg(not(target_arch = "wasm32"))]
fn split_legs(
apply_end: std::time::Instant,
pre_layout: std::time::Instant,
post_layout: std::time::Instant,
) -> (Duration, Duration) {
(
pre_layout.saturating_duration_since(apply_end),
post_layout.saturating_duration_since(pre_layout),
)
}
pub(super) fn emit_batch_stats(
state: Res<DevtoolsState>,
stats: Res<OpApplyStats>,
timers: Res<DevtoolsTimers>,
events: ReactEvents,
mut seen: Local<u64>,
) {
if stats.app_applied_count == *seen {
return;
}
*seen = stats.app_applied_count;
if !state.open {
return;
}
events.send(&DevtoolsBatchStats {
applied_count: stats.applied_count,
last_ops: stats.last_ops,
frame_wait_ms: stats.last_frame_wait.as_secs_f64() * 1000.0,
pre_apply_ms: stats.last_pre_apply.as_secs_f64() * 1000.0,
translate_ms: stats.last_translate.as_secs_f64() * 1000.0,
command_ms: timers.last_command.as_secs_f64() * 1000.0,
layout_ms: timers.last_layout.as_secs_f64() * 1000.0,
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::devtools::DevtoolsConfig;
use crate::devtools::test_util::{drain_events, test_app};
use crate::protocol::outbound::Outbound;
use tokio::sync::mpsc::UnboundedReceiver;
#[test]
fn batch_stats_skip_devtools_only_applies() {
let (mut app, mut rx) = test_app(DevtoolsConfig {
settings_path: None,
..default()
});
app.world_mut().resource_mut::<DevtoolsState>().open = true;
let stats_events = |rx: &mut UnboundedReceiver<Outbound>| {
drain_events(rx)
.into_iter()
.filter(|(name, _)| name == "devtools.batchStats")
.count()
};
{
let mut stats = app.world_mut().resource_mut::<OpApplyStats>();
stats.applied_count = 1;
stats.app_applied_count = 0;
}
app.update();
assert_eq!(
stats_events(&mut rx),
0,
"the panel's own commits must not produce batch stats"
);
{
let mut stats = app.world_mut().resource_mut::<OpApplyStats>();
stats.applied_count = 2;
stats.app_applied_count = 1;
}
app.update();
let stats: Vec<_> = drain_events(&mut rx)
.into_iter()
.filter(|(name, _)| name == "devtools.batchStats")
.collect();
assert_eq!(stats.len(), 1, "an app apply reports once");
assert!(
stats[0].1.get("frame_wait_ms").is_some(),
"batch stats carry the frame-wait leg"
);
}
#[test]
fn split_legs_computes_command_and_layout() {
let t0 = std::time::Instant::now();
let t1 = t0 + Duration::from_millis(5);
let t2 = t1 + Duration::from_millis(7);
assert_eq!(
split_legs(t0, t1, t2),
(Duration::from_millis(5), Duration::from_millis(7))
);
assert_eq!(split_legs(t1, t0, t2), (Duration::ZERO, t2 - t0));
}
}