veloren/common/src/clock.rs
Ben Wallis b499cf2c58 Added egui debug UI - a 100% rust UI framework (similar to imgui) allowing for rapid development of debug interfaces to aid development. This is feature-gated behind the egui-ui feature which is enabled by default but removed for airshipper builds.
Included in the initial implementation is an entity browser which lists all entities in the client ECS, an entity component viewer which shows select components belonging to the selected entity including character state information, and a simple frame time graph.

This MR also includes an extraction of the animation hot reloading code which has been reused for egui to allow for hot-reloading of the egui interface to allow rapid development of the UI with realtime feedback upon save as is the case with aninmations. This is feature-gated behind the `hot-egui` feature which is not enabled by default due to the extra startup time that it adds.
2021-07-04 09:47:18 +00:00

175 lines
6.6 KiB
Rust

use common_base::span;
use ordered_float::NotNan;
use std::{
collections::VecDeque,
time::{Duration, Instant},
};
/// This Clock tries to make this tick a constant time by sleeping the rest of
/// the tick
/// - if we actually took less time than we planned: sleep and return planned
/// time
/// - if we ran behind: don't sleep and return actual time
/// We DON'T do any fancy averaging of the deltas for tick for 2 reasons:
/// - all Systems have to work based on `dt` and we cannot assume that this is
/// const through all ticks
/// - when we have a slow tick, a lag, it doesn't help that we have 10 fast
/// ticks directly afterwards
/// We return a smoothed version for display only!
pub struct Clock {
/// This is the dt that the Clock tries to archive with each call of tick.
target_dt: Duration,
/// Last time `tick` was called
last_sys_time: Instant,
/// Will be calculated in `tick` returns the dt used by the next iteration
/// of the main loop
last_dt: Duration,
/// Summed up `last_dt`
total_tick_time: Duration,
// Stats only
// uses f32 so we have enough precision to display fps values while saving space
// This is in seconds
last_dts: VecDeque<NotNan<f32>>,
last_dts_sorted: Vec<NotNan<f32>>,
last_busy_dts: VecDeque<NotNan<f32>>,
stats: ClockStats,
}
pub struct ClockStats {
/// Busy dt is the part of the tick that we didn't sleep.
/// e.g. the total tick is 33ms, including 25ms sleeping. then this returns
/// 8ms
/// This is in seconds
pub average_busy_dt: Duration,
/// avg over the last NUMBER_OF_OLD_DELTAS_KEPT ticks
pub average_tps: f64,
/// = 50% percentile
pub median_tps: f64,
/// lowest 10% of the frames
pub percentile_90_tps: f64,
/// lowest 5% of the frames
pub percentile_95_tps: f64,
/// lowest 1% of the frames
pub percentile_99_tps: f64,
}
const NUMBER_OF_OLD_DELTAS_KEPT: usize = 100;
const NUMBER_OF_DELTAS_COMPARED: usize = 5;
impl Clock {
pub fn new(target_dt: Duration) -> Self {
Self {
target_dt,
last_sys_time: Instant::now(),
last_dt: target_dt,
total_tick_time: Duration::default(),
last_dts: VecDeque::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
last_dts_sorted: Vec::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
last_busy_dts: VecDeque::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
stats: ClockStats::new(&[], &VecDeque::new()),
}
}
pub fn set_target_dt(&mut self, target_dt: Duration) { self.target_dt = target_dt; }
pub fn stats(&self) -> &ClockStats { &self.stats }
pub fn dt(&self) -> Duration { self.last_dt }
pub fn get_stable_dt(&self) -> Duration {
let stable_dt = Duration::from_secs_f32(
self.last_dts
.iter()
.skip(self.last_dts.len() - NUMBER_OF_DELTAS_COMPARED)
.min()
.map_or(self.last_dt.as_secs_f32(), |t| t.into_inner()),
);
if self.last_dts.len() >= NUMBER_OF_DELTAS_COMPARED && self.last_dt > 2 * stable_dt {
tracing::trace!(?self.last_dt, ?self.total_tick_time, "lag spike detected, unusually slow tick");
stable_dt
} else {
self.last_dt
}
}
/// Do not modify without asking @xMAC94x first!
pub fn tick(&mut self) {
span!(_guard, "tick", "Clock::tick");
span!(guard, "clock work");
let current_sys_time = Instant::now();
let busy_delta = current_sys_time.duration_since(self.last_sys_time);
// Maintain TPS
self.last_dts_sorted = self.last_dts.iter().copied().collect();
self.last_dts_sorted.sort_unstable();
self.stats = ClockStats::new(&self.last_dts_sorted, &self.last_busy_dts);
drop(guard);
// Attempt to sleep to fill the gap.
if let Some(sleep_dur) = self.target_dt.checked_sub(busy_delta) {
spin_sleep::sleep(sleep_dur);
}
let after_sleep_sys_time = Instant::now();
self.last_dt = after_sleep_sys_time.duration_since(self.last_sys_time);
if self.last_dts.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
self.last_dts.pop_front();
}
if self.last_busy_dts.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
self.last_busy_dts.pop_front();
}
self.last_dts.push_back(
NotNan::new(self.last_dt.as_secs_f32())
.expect("Duration::as_secs_f32 never returns NaN"),
);
self.last_busy_dts.push_back(
NotNan::new(busy_delta.as_secs_f32()).expect("Duration::as_secs_f32 never returns NaN"),
);
self.total_tick_time += self.last_dt;
self.last_sys_time = after_sleep_sys_time;
}
}
impl ClockStats {
fn new(sorted: &[NotNan<f32>], busy_dt_list: &VecDeque<NotNan<f32>>) -> Self {
let average_frame_time =
sorted.iter().sum::<NotNan<f32>>().into_inner() / sorted.len().max(1) as f32;
let average_busy_dt = busy_dt_list.iter().sum::<NotNan<f32>>().into_inner()
/ busy_dt_list.len().max(1) as f32;
let average_tps = 1.0 / average_frame_time as f64;
let (median_tps, percentile_90_tps, percentile_95_tps, percentile_99_tps) =
if sorted.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
let median_frame_time = *sorted[sorted.len() / 2];
let percentile_90_frame_time =
*sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.1) as usize];
let percentile_95_frame_time =
*sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.05) as usize];
let percentile_99_frame_time =
*sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.01) as usize];
let median_tps = 1.0 / median_frame_time as f64;
let percentile_90_tps = 1.0 / percentile_90_frame_time as f64;
let percentile_95_tps = 1.0 / percentile_95_frame_time as f64;
let percentile_99_tps = 1.0 / percentile_99_frame_time as f64;
(
median_tps,
percentile_90_tps,
percentile_95_tps,
percentile_99_tps,
)
} else {
let avg_tps = 1.0 / average_busy_dt as f64;
(avg_tps, avg_tps, avg_tps, avg_tps)
};
Self {
average_busy_dt: Duration::from_secs_f32(average_busy_dt),
average_tps,
median_tps,
percentile_90_tps,
percentile_95_tps,
percentile_99_tps,
}
}
}