Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Add Exponential Moving Average into diagnostics #4992

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion crates/bevy_diagnostic/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub struct Diagnostic {
pub suffix: Cow<'static, str>,
history: VecDeque<DiagnosticMeasurement>,
sum: f64,
ema: f64,
ema_smoothing_factor: f64,
max_history_length: usize,
pub is_enabled: bool,
}
Expand All @@ -45,6 +47,15 @@ impl Diagnostic {
/// Add a new value as a [`DiagnosticMeasurement`]. Its timestamp will be [`Instant::now`].
pub fn add_measurement(&mut self, value: f64) {
let time = Instant::now();

if let Some(previous) = self.measurement() {
let delta = (time - previous.time).as_secs_f64();
let alpha = (delta / self.ema_smoothing_factor).clamp(0.0, 1.0);
self.ema += alpha * (value - self.ema);
} else {
self.ema = value;
}

if self.max_history_length > 1 {
if self.history.len() == self.max_history_length {
if let Some(removed_diagnostic) = self.history.pop_front() {
Expand All @@ -57,6 +68,7 @@ impl Diagnostic {
self.history.clear();
self.sum = value;
}

self.history
.push_back(DiagnosticMeasurement { time, value });
}
Expand All @@ -83,6 +95,8 @@ impl Diagnostic {
history: VecDeque::with_capacity(max_history_length),
max_history_length,
sum: 0.0,
ema: 0.0,
ema_smoothing_factor: 2.0 / 21.0,
is_enabled: true,
}
}
Expand All @@ -94,6 +108,22 @@ impl Diagnostic {
self
}

/// The smoothing factor used for the exponential smoothing used for
/// [`smoothed`](Self::smoothed).
///
/// If measurements come in less fequently than `smoothing_factor` seconds
/// apart, no smoothing will be applied. As measurements come in more
/// frequently, the smoothing takes a greater effect such that it takes
/// approximately `smoothing_factor` seconds for 83% of an instantaneous
/// change in measurement to e reflected in the smoothed value.
///
/// A smoothing factor of 0.0 will effectively disable smoothing.
#[must_use]
pub fn with_smoothing_factor(mut self, smoothing_factor: f64) -> Self {
CAD97 marked this conversation as resolved.
Show resolved Hide resolved
self.ema_smoothing_factor = smoothing_factor;
self
}

/// Get the latest measurement from this diagnostic.
#[inline]
pub fn measurement(&self) -> Option<&DiagnosticMeasurement> {
Expand All @@ -105,7 +135,7 @@ impl Diagnostic {
self.measurement().map(|measurement| measurement.value)
}

/// Return the mean (average) of this diagnostic's values.
/// Return the simple moving average of this diagnostic's recent values.
/// N.B. this a cheap operation as the sum is cached.
pub fn average(&self) -> Option<f64> {
if !self.history.is_empty() {
Expand All @@ -115,6 +145,19 @@ impl Diagnostic {
}
}

/// Return the exponential moving average of this diagnostic.
///
/// This is by default tuned to behave reasonably well for a typical
/// measurement that changes every frame such as frametime. This can be
/// adjusted using [`with_smoothing_factor`](Self::with_smoothing_factor).
pub fn smoothed(&self) -> Option<f64> {
if !self.history.is_empty() {
Some(self.ema)
} else {
None
}
}

/// Return the number of elements for this diagnostic.
pub fn history_len(&self) -> usize {
self.history.len()
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ impl FrameTimeDiagnosticsPlugin {
pub fn setup_system(mut diagnostics: ResMut<Diagnostics>) {
diagnostics.add(Diagnostic::new(Self::FRAME_TIME, "frame_time", 20).with_suffix("ms"));
diagnostics.add(Diagnostic::new(Self::FPS, "fps", 20));
diagnostics.add(Diagnostic::new(Self::FRAME_COUNT, "frame_count", 1));
diagnostics
.add(Diagnostic::new(Self::FRAME_COUNT, "frame_count", 1).with_smoothing_factor(0.0));
}

pub fn diagnostic_system(
Expand Down
10 changes: 5 additions & 5 deletions crates/bevy_diagnostic/src/log_diagnostics_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,16 @@ impl LogDiagnosticsPlugin {
}

fn log_diagnostic(diagnostic: &Diagnostic) {
if let Some(value) = diagnostic.value() {
if let Some(value) = diagnostic.smoothed() {
if diagnostic.get_max_history_length() > 1 {
if let Some(average) = diagnostic.average() {
info!(
target: "bevy diagnostic",
// Suffix is only used for 's' as in seconds currently,
// so we reserve one column for it; however,
// Do not reserve one column for the suffix in the average
// Suffix is only used for 's' or 'ms' currently,
// so we reserve two columns for it; however,
// Do not reserve columns for the suffix in the average
// The ) hugging the value is more aesthetically pleasing
"{name:<name_width$}: {value:>11.6}{suffix:1} (avg {average:>.6}{suffix:})",
"{name:<name_width$}: {value:>11.6}{suffix:2} (avg {average:>.6}{suffix:})",
name = diagnostic.name,
suffix = diagnostic.suffix,
name_width = crate::MAX_DIAGNOSTIC_NAME_WIDTH,
Expand Down
55 changes: 27 additions & 28 deletions examples/stress_tests/bevymark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,35 +96,28 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {

let texture = asset_server.load("branding/icon.png");

let text_section = move |color, value: &str| {
TextSection::new(
value,
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color,
},
)
};

commands.spawn(Camera2dBundle::default());
commands.spawn((
TextBundle::from_sections([
TextSection::new(
"Bird Count: ",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 0.0),
},
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
}),
TextSection::new(
"\nAverage FPS: ",
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 0.0),
},
),
TextSection::from_style(TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 40.0,
color: Color::rgb(0.0, 1.0, 1.0),
}),
text_section(Color::GREEN, "Bird Count"),
text_section(Color::CYAN, ""),
text_section(Color::GREEN, "\nFPS (raw): "),
text_section(Color::CYAN, ""),
text_section(Color::GREEN, "\nFPS (SMA): "),
text_section(Color::CYAN, ""),
text_section(Color::GREEN, "\nFPS (EMA): "),
text_section(Color::CYAN, ""),
])
.with_style(Style {
position_type: PositionType::Absolute,
Expand Down Expand Up @@ -261,8 +254,14 @@ fn counter_system(
}

if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
text.sections[3].value = format!("{average:.2}");
if let Some(raw) = fps.value() {
text.sections[3].value = format!("{raw:.2}");
}
if let Some(sma) = fps.average() {
text.sections[5].value = format!("{sma:.2}");
}
if let Some(ema) = fps.smoothed() {
text.sections[7].value = format!("{ema:.2}");
}
};
}
4 changes: 2 additions & 2 deletions examples/ui/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ fn text_color_system(time: Res<Time>, mut query: Query<&mut Text, With<ColorText
fn text_update_system(diagnostics: Res<Diagnostics>, mut query: Query<&mut Text, With<FpsText>>) {
for mut text in &mut query {
if let Some(fps) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(average) = fps.average() {
if let Some(value) = fps.smoothed() {
// Update the value of the second section
text.sections[1].value = format!("{average:.2}");
text.sections[1].value = format!("{value:.2}");
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions examples/ui/text_debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,16 @@ fn change_text_system(
for mut text in &mut query {
let mut fps = 0.0;
if let Some(fps_diagnostic) = diagnostics.get(FrameTimeDiagnosticsPlugin::FPS) {
if let Some(fps_avg) = fps_diagnostic.average() {
fps = fps_avg;
if let Some(fps_smoothed) = fps_diagnostic.smoothed() {
fps = fps_smoothed;
}
}

let mut frame_time = time.delta_seconds_f64();
if let Some(frame_time_diagnostic) = diagnostics.get(FrameTimeDiagnosticsPlugin::FRAME_TIME)
{
if let Some(frame_time_avg) = frame_time_diagnostic.average() {
frame_time = frame_time_avg;
if let Some(frame_time_smoothed) = frame_time_diagnostic.smoothed() {
frame_time = frame_time_smoothed;
}
}

Expand Down