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 GPU_WAIT_BEARER: &str = "GraphicsSystem";
#[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,
}
#[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 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>,
}
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,
})
}
}
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);
}
run.system_names
.iter()
.enumerate()
.filter(|(_, name)| !name.is_empty())
.map(|(slot, name)| system_us(sample, slot, name))
.sum()
}
fn system_us(sample: &FrameSample, slot: usize, name: &str) -> u32 {
let step = sample.system_us[slot];
if name == GPU_WAIT_BEARER {
step.saturating_sub(sample.gpu_wait_us)
} else {
step
}
}
fn system_costs(frames: &[&FrameSample], run: &FrameRun) -> Vec<SystemCost> {
let means: Vec<(&String, u32)> = run
.system_names
.iter()
.enumerate()
.filter(|(_, name)| !name.is_empty())
.map(|(slot, name)| {
let mean_us = mean_u32(frames.iter().map(|s| system_us(s, slot, name)));
(name, mean_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.clone(),
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
},
})
})
.collect();
shares.sort_unstable_by_key(|p| core::cmp::Reverse(p.median_us));
shares.truncate(PASSES_REPORTED);
let listed: u32 = shares.iter().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,
});
}
shares
}
#[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,
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_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_graphics_systems_span_has_the_blocked_on_gpu_wait_taken_out_of_it() {
let mut only = sample(0.0, Some(0), 10_000);
only.gpu_wait_us = 9_000;
only.system_us[0] = 300;
only.system_us[1] = 9_400;
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, 700);
let systems = &report.overall.systems;
assert_eq!(systems[0].name, "GraphicsSystem");
assert_eq!(systems[0].mean_us, 400);
assert_eq!(systems[1].mean_us, 300);
assert!((systems[0].share - 400.0 / 700.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] = 11_800;
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 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);
}
}