use concinnity_core::render::render_graph::PassId;
use serde::Serialize;
use crate::frame_report::sample::{FrameRun, FrameSample};
use crate::frame_report::stats::{Distribution, mean_u32};
const PASSES_REPORTED: usize = 8;
const SYSTEMS_REPORTED: usize = 6;
const UNATTRIBUTED: &str = "unattributed";
const SYSTEM_FLOOR_SHARE: f32 = 0.01;
const RENDER_SUBMIT: &str = "render submit";
const SLOW_FRAMES_REPORTED: usize = 3;
const SLOW_FRAME_ROWS: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct ReduceOptions {
pub warmup_seconds: f32,
pub budget_us: u32,
}
impl Default for ReduceOptions {
fn default() -> Self {
Self {
warmup_seconds: 2.0,
budget_us: 16_667,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct PassShare {
pub name: String,
pub median_us: u32,
pub share: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub within: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SystemCost {
pub name: String,
pub mean_us: u32,
pub share: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct FrameCost {
pub name: String,
pub us: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SlowFrame {
pub run_seconds: f32,
pub segment: String,
pub frame_us: u32,
pub cpu_us: u32,
pub gpu_wait_us: u32,
pub gpu_frame_us: u32,
pub cpu: Vec<FrameCost>,
pub passes: Vec<FrameCost>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SegmentReport {
pub name: String,
pub seconds: f32,
pub frame: Distribution,
pub cpu: Distribution,
pub gpu: Distribution,
pub gpu_wait_mean_us: u32,
pub draw_calls_mean: u32,
pub objects_mean: u32,
pub vram_peak_bytes: u64,
pub passes: Vec<PassShare>,
pub systems: Vec<SystemCost>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Report {
pub completed: bool,
pub warmup_dropped: usize,
pub budget_us: u32,
pub overall: SegmentReport,
pub segments: Vec<SegmentReport>,
pub slowest: Vec<SlowFrame>,
}
impl Report {
pub fn of(run: &FrameRun, options: ReduceOptions) -> Option<Self> {
let measured: Vec<&FrameSample> = run
.samples
.iter()
.filter(|s| s.run_seconds >= options.warmup_seconds)
.collect();
if measured.is_empty() {
return None;
}
let mut segments = Vec::new();
for (index, name) in segment_order(&measured, run) {
let frames: Vec<&FrameSample> = measured
.iter()
.copied()
.filter(|s| s.segment == index)
.collect();
segments.push(summarize(name, &frames, run, options));
}
Some(Self {
completed: run.completed,
warmup_dropped: run.samples.len() - measured.len(),
budget_us: options.budget_us,
overall: summarize("all".to_string(), &measured, run, options),
segments,
slowest: slowest_frames(&measured, run),
})
}
}
fn slowest_frames(measured: &[&FrameSample], run: &FrameRun) -> Vec<SlowFrame> {
let mut by_time: Vec<&FrameSample> = measured.to_vec();
by_time.sort_by_key(|s| core::cmp::Reverse(s.frame_us));
by_time.truncate(SLOW_FRAMES_REPORTED);
by_time
.into_iter()
.map(|sample| SlowFrame {
run_seconds: sample.run_seconds,
segment: run.segment_name(sample.segment).to_string(),
frame_us: sample.frame_us,
cpu_us: stepped_us(sample, run),
gpu_wait_us: sample.gpu_wait_us,
gpu_frame_us: sample.gpu_frame_us,
cpu: largest(
named_slots(&run.system_names)
.map(|(slot, name)| (name, sample.system_us[slot]))
.chain(core::iter::once((RENDER_SUBMIT, sample.render_cpu_us))),
),
passes: {
let us = |name: &str| {
named_slots(&run.pass_names)
.find(|(_, n)| *n == name)
.map_or(0, |(slot, _)| sample.pass_us[slot])
};
largest(
named_slots(&run.pass_names)
.filter(|(_, name)| counted_in(name, |outer| us(outer) > 0).is_none())
.map(|(slot, name)| (name, sample.pass_us[slot])),
)
},
})
.collect()
}
fn largest<'a>(costs: impl Iterator<Item = (&'a str, u32)>) -> Vec<FrameCost> {
let mut rows: Vec<FrameCost> = costs
.filter(|(_, us)| *us > 0)
.map(|(name, us)| FrameCost {
name: name.to_string(),
us,
})
.collect();
rows.sort_by_key(|row| core::cmp::Reverse(row.us));
rows.truncate(SLOW_FRAME_ROWS);
rows
}
fn counted_in(name: &str, ran: impl Fn(&str) -> bool) -> Option<&'static str> {
PassId::from_name(name)
.and_then(PassId::enclosing)
.map(PassId::name)
.filter(|outer| ran(outer))
}
fn named_slots(names: &[String]) -> impl Iterator<Item = (usize, &str)> {
names
.iter()
.enumerate()
.filter(|(_, name)| !name.is_empty())
.map(|(slot, name)| (slot, name.as_str()))
}
fn segment_order(measured: &[&FrameSample], run: &FrameRun) -> Vec<(Option<u32>, String)> {
let mut seen: Vec<(Option<u32>, String)> = Vec::new();
for sample in measured {
if !seen.iter().any(|(index, _)| *index == sample.segment) {
seen.push((sample.segment, run.segment_name(sample.segment).to_string()));
}
}
seen
}
fn summarize(
name: String,
frames: &[&FrameSample],
run: &FrameRun,
options: ReduceOptions,
) -> SegmentReport {
let mut frame_us: Vec<u32> = frames.iter().map(|s| s.frame_us).collect();
let mut cpu_us: Vec<u32> = frames.iter().map(|s| stepped_us(s, run)).collect();
let mut gpu_us: Vec<u32> = frames.iter().map(|s| s.gpu_frame_us).collect();
let gpu = Distribution::of(&mut gpu_us, options.budget_us);
let cpu = Distribution::of(&mut cpu_us, options.budget_us);
let frame = Distribution::of(&mut frame_us, options.budget_us);
SegmentReport {
name,
seconds: span_seconds(frames),
frame,
cpu,
gpu,
gpu_wait_mean_us: mean_u32(frames.iter().map(|s| s.gpu_wait_us)),
draw_calls_mean: mean_u32(frames.iter().map(|s| s.draw_calls)),
objects_mean: mean_u32(frames.iter().map(|s| s.objects)),
vram_peak_bytes: frames.iter().map(|s| s.vram_bytes).max().unwrap_or(0),
passes: pass_shares(frames, run, gpu.p50_us, options.budget_us),
systems: system_costs(frames, run),
}
}
fn stepped_us(sample: &FrameSample, run: &FrameRun) -> u32 {
if run.system_names.is_empty() {
return sample.frame_us.saturating_sub(sample.gpu_wait_us);
}
named_slots(&run.system_names)
.map(|(slot, _)| sample.system_us[slot])
.fold(sample.render_cpu_us, u32::saturating_add)
}
fn system_costs(frames: &[&FrameSample], run: &FrameRun) -> Vec<SystemCost> {
if run.system_names.is_empty() {
return Vec::new();
}
let means: Vec<(&str, u32)> = named_slots(&run.system_names)
.map(|(slot, name)| (name, mean_u32(frames.iter().map(|s| s.system_us[slot]))))
.chain(core::iter::once((
RENDER_SUBMIT,
mean_u32(frames.iter().map(|s| s.render_cpu_us)),
)))
.collect();
let stepped: u32 = means.iter().map(|(_, us)| us).sum();
if stepped == 0 {
return Vec::new();
}
let mut costs: Vec<SystemCost> = means
.into_iter()
.filter_map(|(name, mean_us)| {
let share = mean_us as f32 / stepped as f32;
if share < SYSTEM_FLOOR_SHARE {
return None;
}
Some(SystemCost {
name: name.to_string(),
mean_us,
share,
})
})
.collect();
costs.sort_unstable_by_key(|c| core::cmp::Reverse(c.mean_us));
costs.truncate(SYSTEMS_REPORTED);
costs
}
fn span_seconds(frames: &[&FrameSample]) -> f32 {
match (frames.first(), frames.last()) {
(Some(first), Some(last)) => last.run_seconds - first.run_seconds,
_ => 0.0,
}
}
fn pass_shares(
frames: &[&FrameSample],
run: &FrameRun,
gpu_median_us: u32,
budget_us: u32,
) -> Vec<PassShare> {
let mut column: Vec<u32> = Vec::with_capacity(frames.len());
let mut shares: Vec<PassShare> = run
.pass_names
.iter()
.enumerate()
.filter(|(_, name)| !name.is_empty())
.filter_map(|(slot, name)| {
column.clear();
column.extend(frames.iter().map(|s| s.pass_us[slot]));
let median_us = Distribution::of(&mut column, budget_us).p50_us;
if median_us == 0 {
return None;
}
Some(PassShare {
name: name.clone(),
median_us,
share: if gpu_median_us == 0 {
0.0
} else {
median_us as f32 / gpu_median_us as f32
},
within: None,
})
})
.collect();
shares.sort_unstable_by_key(|p| core::cmp::Reverse(p.median_us));
let mut shares = nest(shares);
let listed: u32 = shares
.iter()
.filter(|p| p.within.is_none())
.map(|p| p.median_us)
.sum();
let rest = gpu_median_us.saturating_sub(listed);
if rest > 0 && !shares.is_empty() {
shares.push(PassShare {
name: UNATTRIBUTED.to_string(),
median_us: rest,
share: rest as f32 / gpu_median_us as f32,
within: None,
});
}
shares
}
fn nest(shares: Vec<PassShare>) -> Vec<PassShare> {
let ran: Vec<String> = shares.iter().map(|p| p.name.clone()).collect();
let within = |name: &str| counted_in(name, |outer| ran.iter().any(|n| n == outer));
let (mut inner, top): (Vec<PassShare>, Vec<PassShare>) =
shares.into_iter().partition(|p| within(&p.name).is_some());
let mut out = Vec::with_capacity(top.len() + inner.len());
for pass in top.into_iter().take(PASSES_REPORTED) {
let outer = pass.name.clone();
out.push(pass);
for mut p in inner.extract_if(.., |p| within(&p.name) == Some(outer.as_str())) {
p.within = Some(outer.clone());
out.push(p);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame_report::sample::MAX_SYSTEM_TIMINGS;
use concinnity_core::profile::MAX_PASS_TIMINGS;
fn sample(run_seconds: f32, segment: Option<u32>, frame_us: u32) -> FrameSample {
FrameSample {
run_seconds,
segment,
frame_us,
gpu_frame_us: frame_us / 2,
gpu_wait_us: 100,
render_cpu_us: 0,
draw_calls: 50,
objects: 400,
vram_bytes: 1 << 20,
pass_us: [0; MAX_PASS_TIMINGS],
system_us: [0; MAX_SYSTEM_TIMINGS],
}
}
fn run_of(samples: Vec<FrameSample>) -> FrameRun {
FrameRun {
samples,
segments: vec!["shadows".to_string(), "rays".to_string()],
pass_names: Vec::new(),
system_names: Vec::new(),
completed: true,
}
}
fn no_warmup() -> ReduceOptions {
ReduceOptions {
warmup_seconds: 0.0,
..Default::default()
}
}
#[test]
fn the_default_discard_and_budget_are_the_documented_ones() {
let o = ReduceOptions::default();
assert_eq!(o.warmup_seconds, 2.0);
assert_eq!(o.budget_us, 16_667);
}
#[test]
fn a_run_that_is_all_warmup_reduces_to_nothing_rather_than_to_zeroes() {
let run = run_of(vec![sample(0.5, Some(0), 16_000)]);
assert!(Report::of(&run, ReduceOptions::default()).is_none());
}
#[test]
fn warmup_frames_are_dropped_and_counted() {
let run = run_of(vec![
sample(0.5, Some(0), 90_000),
sample(1.9, Some(0), 80_000),
sample(2.0, Some(0), 10_000),
sample(3.0, Some(0), 12_000),
]);
let report = Report::of(&run, ReduceOptions::default()).expect("measured frames");
assert_eq!(report.warmup_dropped, 2);
assert_eq!(report.overall.frame.count, 2);
assert_eq!(report.overall.frame.max_us, 12_000);
}
#[test]
fn segments_come_out_in_the_order_the_path_entered_them() {
let run = run_of(vec![
sample(0.0, Some(1), 10_000),
sample(1.0, Some(0), 20_000),
sample(2.0, Some(1), 11_000),
]);
let report = Report::of(&run, no_warmup()).expect("measured frames");
let names: Vec<&str> = report.segments.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, ["rays", "shadows"]);
}
#[test]
fn a_segment_summarizes_only_its_own_frames() {
let run = run_of(vec![
sample(0.0, Some(0), 10_000),
sample(1.0, Some(0), 10_000),
sample(2.0, Some(1), 40_000),
sample(3.0, Some(1), 40_000),
]);
let report = Report::of(&run, no_warmup()).expect("measured frames");
let shadows = &report.segments[0];
let rays = &report.segments[1];
assert_eq!(shadows.frame.mean_us, 10_000);
assert_eq!(rays.frame.mean_us, 40_000);
assert_eq!((shadows.frame.count, rays.frame.count), (2, 2));
assert_eq!(report.overall.frame.count, 4);
assert_eq!(report.overall.frame.mean_us, 25_000);
}
#[test]
fn a_segments_span_is_its_own_track_time() {
let run = run_of(vec![
sample(0.0, Some(0), 10_000),
sample(4.0, Some(0), 10_000),
sample(5.0, Some(1), 10_000),
]);
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert_eq!(report.segments[0].seconds, 4.0);
assert_eq!(report.segments[1].seconds, 0.0);
}
#[test]
fn frames_in_no_segment_are_reported_under_a_placeholder() {
let run = run_of(vec![sample(0.0, None, 10_000)]);
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert_eq!(report.segments[0].name, "(unnamed)");
}
#[test]
fn passes_are_ranked_by_cost_and_carry_their_share_of_the_gpu_frame() {
let mut first = sample(0.0, Some(0), 20_000);
first.pass_us[0] = 1_000;
first.pass_us[1] = 6_000;
first.pass_us[2] = 3_000;
let mut run = run_of(vec![first]);
run.pass_names = vec!["shadow".to_string(), "main".to_string(), "ssao".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let passes = &report.overall.passes;
assert_eq!(passes[0].name, "main");
assert_eq!(passes[0].median_us, 6_000);
assert!((passes[0].share - 0.6).abs() < 1e-4);
assert_eq!(passes[1].name, "ssao");
assert_eq!(passes[2].name, "shadow");
assert_eq!(passes.len(), 3);
}
#[test]
fn what_the_listed_passes_do_not_cover_is_reported_as_its_own_row() {
let mut only = sample(0.0, Some(0), 20_000);
only.pass_us[0] = 2_500;
let mut run = run_of(vec![only]);
run.pass_names = vec!["main".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let passes = &report.overall.passes;
assert_eq!(passes.len(), 2);
assert_eq!(passes[1].name, UNATTRIBUTED);
assert_eq!(passes[1].median_us, 7_500);
assert!((passes.iter().map(|p| p.share).sum::<f32>() - 1.0).abs() < 1e-4);
}
#[test]
fn overlapping_passes_that_outlast_the_frame_leave_no_remainder() {
let mut only = sample(0.0, Some(0), 20_000);
only.pass_us[0] = 8_000;
only.pass_us[1] = 7_000;
let mut run = run_of(vec![only]);
run.pass_names = vec!["main".to_string(), "async".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let names: Vec<&str> = report
.overall
.passes
.iter()
.map(|p| p.name.as_str())
.collect();
assert_eq!(names, ["main", "async"]);
}
#[test]
fn a_pass_timed_inside_another_is_listed_under_it_and_counted_once() {
let mut nested = sample(0.0, Some(0), 20_000);
nested.pass_us[0] = 3_000;
nested.pass_us[1] = 6_000;
nested.pass_us[2] = 1_000;
let mut run = run_of(vec![nested]);
run.pass_names = vec!["shadow".to_string(), "main".to_string(), "sky".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let rows: Vec<(&str, Option<&str>, u32)> = report
.overall
.passes
.iter()
.map(|p| (p.name.as_str(), p.within.as_deref(), p.median_us))
.collect();
assert_eq!(
rows,
[
("main", None, 6_000),
("sky", Some("main"), 1_000),
("shadow", None, 3_000),
(UNATTRIBUTED, None, 1_000),
]
);
let top: f32 = report
.overall
.passes
.iter()
.filter(|p| p.within.is_none())
.map(|p| p.share)
.sum();
assert!((top - 1.0).abs() < 1e-4);
let mut folded = sample(0.0, Some(0), 20_000);
folded.pass_us[0] = 3_000;
folded.pass_us[1] = 6_000;
let mut run = run_of(vec![folded]);
run.pass_names = vec!["shadow".to_string(), "main".to_string(), "sky".to_string()];
let metal = Report::of(&run, no_warmup()).expect("measured frames");
let unattributed = |r: &Report| r.overall.passes.last().map(|p| p.median_us);
assert_eq!(unattributed(&metal), unattributed(&report));
}
#[test]
fn a_nested_pass_without_its_enclosing_pass_stands_on_its_own() {
let mut only = sample(0.0, Some(0), 20_000);
only.pass_us[1] = 4_000;
let mut run = run_of(vec![only]);
run.pass_names = vec!["main".to_string(), "sky".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let passes = &report.overall.passes;
assert_eq!(passes[0].name, "sky");
assert_eq!(passes[0].within, None);
assert_eq!(passes[1].median_us, 6_000);
}
#[test]
fn a_slow_frame_lists_a_nested_pass_whose_enclosing_pass_did_not_run() {
let mut slow = sample(0.0, Some(0), 20_000);
slow.pass_us[1] = 1_000;
slow.pass_us[2] = 3_000;
let mut run = run_of(vec![slow]);
run.pass_names = vec!["main".to_string(), "sky".to_string(), "shadow".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let names: Vec<&str> = report.slowest[0]
.passes
.iter()
.map(|p| p.name.as_str())
.collect();
assert_eq!(names, ["shadow", "sky"]);
let segment: Vec<&str> = report
.overall
.passes
.iter()
.filter(|p| p.within.is_none())
.map(|p| p.name.as_str())
.collect();
assert_eq!(segment, ["shadow", "sky", UNATTRIBUTED]);
}
#[test]
fn a_slow_frame_lists_only_the_passes_that_split_its_frame() {
let mut slow = sample(0.0, Some(0), 20_000);
slow.pass_us[0] = 6_000;
slow.pass_us[1] = 1_000;
let mut run = run_of(vec![slow]);
run.pass_names = vec!["main".to_string(), "sky".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let names: Vec<&str> = report.slowest[0]
.passes
.iter()
.map(|p| p.name.as_str())
.collect();
assert_eq!(names, ["main"]);
}
#[test]
fn a_pass_that_never_ran_is_left_out_rather_than_listed_as_zero() {
let mut only = sample(0.0, Some(0), 20_000);
only.pass_us[0] = 10_000;
let mut run = run_of(vec![only]);
run.pass_names = vec!["main".to_string(), "fog".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let names: Vec<&str> = report
.overall
.passes
.iter()
.map(|p| p.name.as_str())
.collect();
assert_eq!(names, ["main"]);
}
#[test]
fn pass_shares_stay_finite_when_the_backend_reports_no_gpu_time() {
let mut only = sample(0.0, Some(0), 20_000);
only.gpu_frame_us = 0;
only.pass_us[0] = 5_000;
let mut run = run_of(vec![only]);
run.pass_names = vec!["main".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert_eq!(report.overall.passes[0].share, 0.0);
}
#[test]
fn systems_are_ranked_by_cost_and_measured_against_what_they_all_stepped() {
let mut only = sample(0.0, Some(0), 10_000);
only.gpu_wait_us = 0;
only.system_us[0] = 400;
only.system_us[1] = 2_500;
let mut run = run_of(vec![only]);
run.system_names = vec!["PhysicsSystem".to_string(), "GraphicsSystem".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let systems = &report.overall.systems;
assert_eq!(systems[0].name, "GraphicsSystem");
assert_eq!(systems[0].mean_us, 2_500);
assert!((systems[0].share - 2_500.0 / 2_900.0).abs() < 1e-4);
assert_eq!(systems[1].name, "PhysicsSystem");
assert!((systems.iter().map(|s| s.share).sum::<f32>() - 1.0).abs() < 1e-4);
}
#[test]
fn the_render_submission_is_a_row_of_its_own_beside_the_systems() {
let mut only = sample(0.0, Some(0), 10_000);
only.gpu_wait_us = 9_000;
only.render_cpu_us = 500;
only.system_us[0] = 300;
only.system_us[1] = 200;
let mut run = run_of(vec![only]);
run.system_names = vec!["PhysicsSystem".to_string(), "GraphicsSystem".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert_eq!(report.overall.cpu.mean_us, 1_000);
let systems = &report.overall.systems;
assert_eq!(systems[0].name, RENDER_SUBMIT);
assert_eq!(systems[0].mean_us, 500);
assert!((systems[0].share - 0.5).abs() < 1e-4);
assert!((systems.iter().map(|s| s.share).sum::<f32>() - 1.0).abs() < 1e-4);
}
#[test]
fn a_run_that_named_no_systems_falls_back_to_the_frame_less_the_wait() {
let mut fast = sample(0.0, Some(0), 10_000);
fast.gpu_wait_us = 2_000;
let mut slow = sample(1.0, Some(0), 20_000);
slow.gpu_wait_us = 1_000;
let report = Report::of(&run_of(vec![fast, slow]), no_warmup()).expect("measured frames");
assert!(run_of(vec![]).system_names.is_empty(), "nothing named");
assert_eq!(report.overall.frame.mean_us, 15_000);
assert_eq!(report.overall.cpu.mean_us, 13_500);
assert_eq!(report.overall.cpu.max_us, 19_000);
}
#[test]
fn a_tick_that_outlasts_the_wait_reads_as_cpu_work_rather_than_as_zero() {
let mut busy = sample(0.0, Some(0), 12_000);
busy.gpu_wait_us = 11_500;
busy.system_us[0] = 8_000;
busy.system_us[1] = 300;
let mut run = run_of(vec![busy]);
run.system_names = vec!["BehaviorSystem".to_string(), "GraphicsSystem".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert_eq!(report.overall.cpu.mean_us, 8_300);
assert_eq!(report.overall.systems[0].name, "BehaviorSystem");
}
#[test]
fn a_system_that_is_a_rounding_error_beside_the_rest_is_left_out() {
let mut only = sample(0.0, Some(0), 20_000);
only.gpu_wait_us = 0;
only.system_us[0] = 5_000;
only.system_us[1] = 10;
let mut run = run_of(vec![only]);
run.system_names = vec!["PhysicsSystem".to_string(), "AudioSystem".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let names: Vec<&str> = report
.overall
.systems
.iter()
.map(|s| s.name.as_str())
.collect();
assert_eq!(names, ["PhysicsSystem"]);
}
#[test]
fn a_run_whose_systems_were_never_timed_reports_no_breakdown() {
let mut run = run_of(vec![sample(0.0, Some(0), 20_000)]);
run.system_names = vec!["PhysicsSystem".to_string()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert!(report.overall.systems.is_empty());
}
#[test]
fn an_incomplete_run_says_so() {
let mut run = run_of(vec![sample(0.0, Some(0), 10_000)]);
run.completed = false;
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert!(!report.completed);
}
#[test]
fn the_slowest_frames_are_named_slowest_first_and_capped() {
let run = run_of(vec![
sample(0.0, Some(0), 10_000),
sample(1.0, Some(1), 300_000),
sample(2.0, Some(0), 12_000),
sample(3.0, Some(0), 40_000),
sample(4.0, Some(1), 40_000),
]);
let report = Report::of(&run, no_warmup()).expect("measured frames");
let slowest: Vec<(u32, f32, &str)> = report
.slowest
.iter()
.map(|f| (f.frame_us, f.run_seconds, f.segment.as_str()))
.collect();
assert_eq!(
slowest,
[
(300_000, 1.0, "rays"),
(40_000, 3.0, "shadows"),
(40_000, 4.0, "rays")
]
);
}
#[test]
fn warmup_frames_are_never_named_among_the_slowest() {
let run = run_of(vec![
sample(0.5, Some(0), 90_000),
sample(3.0, Some(0), 12_000),
]);
let report = Report::of(&run, ReduceOptions::default()).expect("measured frames");
assert_eq!(report.slowest.len(), 1);
assert_eq!(report.slowest[0].frame_us, 12_000);
}
#[test]
fn a_slow_frame_carries_its_own_cpu_wait_and_pass_breakdown() {
let mut hitch = sample(0.0, Some(0), 300_000);
hitch.gpu_wait_us = 297_000;
hitch.gpu_frame_us = 9_000;
hitch.render_cpu_us = 700;
hitch.system_us[0] = 1_200;
hitch.system_us[1] = 0;
hitch.system_us[2] = 400;
hitch.pass_us[0] = 1_000;
hitch.pass_us[1] = 5_000;
let mut run = run_of(vec![hitch]);
run.system_names = vec![
"PhysicsSystem".to_string(),
"AudioSystem".to_string(),
"GraphicsSystem".to_string(),
];
run.pass_names = vec!["shadow".to_string(), "main".to_string(), String::new()];
let report = Report::of(&run, no_warmup()).expect("measured frames");
let frame = &report.slowest[0];
assert_eq!(frame.frame_us, 300_000);
assert_eq!(frame.gpu_wait_us, 297_000);
assert_eq!(frame.gpu_frame_us, 9_000);
assert_eq!(frame.cpu_us, 2_300);
let cpu: Vec<(&str, u32)> = frame.cpu.iter().map(|c| (c.name.as_str(), c.us)).collect();
assert_eq!(
cpu,
[
("PhysicsSystem", 1_200),
(RENDER_SUBMIT, 700),
("GraphicsSystem", 400)
]
);
let passes: Vec<(&str, u32)> = frame
.passes
.iter()
.map(|p| (p.name.as_str(), p.us))
.collect();
assert_eq!(passes, [("main", 5_000), ("shadow", 1_000)]);
}
#[test]
fn a_slow_frame_lists_only_its_largest_costs() {
let mut busy = sample(0.0, Some(0), 20_000);
let mut run_names = Vec::new();
for slot in 0..6 {
busy.system_us[slot] = 100 * (slot as u32 + 1);
run_names.push(format!("System{slot}"));
}
let mut run = run_of(vec![busy]);
run.system_names = run_names;
let report = Report::of(&run, no_warmup()).expect("measured frames");
let names: Vec<&str> = report.slowest[0]
.cpu
.iter()
.map(|c| c.name.as_str())
.collect();
assert_eq!(names, ["System5", "System4", "System3", "System2"]);
}
#[test]
fn peak_memory_is_the_largest_reading_not_the_last() {
let mut early = sample(0.0, Some(0), 10_000);
early.vram_bytes = 900 << 20;
let run = run_of(vec![early, sample(1.0, Some(0), 10_000)]);
let report = Report::of(&run, no_warmup()).expect("measured frames");
assert_eq!(report.overall.vram_peak_bytes, 900 << 20);
}
}