1use crate::{ObjectRotation, TargetingPolicy};
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum BenchmarkRenderPath {
15 FixedOrbitSession,
17 PersistentSteps,
19}
20
21impl BenchmarkRenderPath {
22 pub fn label(self) -> &'static str {
24 match self {
25 Self::FixedOrbitSession => "fixed-orbit-session",
26 Self::PersistentSteps => "persistent-steps",
27 }
28 }
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct BenchmarkWorkload {
34 pub name: String,
35 pub render_path: BenchmarkRenderPath,
36 pub objects: Vec<String>,
37 pub rotations_per_object: usize,
38 pub viewpoints_per_rotation: usize,
39 pub steps_per_object: Option<usize>,
40 pub total_frames: usize,
41}
42
43impl BenchmarkWorkload {
44 pub fn fixed_orbit(
45 name: impl Into<String>,
46 objects: Vec<String>,
47 rotations_per_object: usize,
48 viewpoints_per_rotation: usize,
49 ) -> Self {
50 let total_frames =
51 fixed_orbit_frame_count(objects.len(), rotations_per_object, viewpoints_per_rotation);
52 Self {
53 name: name.into(),
54 render_path: BenchmarkRenderPath::FixedOrbitSession,
55 objects,
56 rotations_per_object,
57 viewpoints_per_rotation,
58 steps_per_object: None,
59 total_frames,
60 }
61 }
62
63 pub fn persistent_steps(
64 name: impl Into<String>,
65 objects: Vec<String>,
66 steps_per_object: usize,
67 ) -> Self {
68 let total_frames = objects.len().saturating_mul(steps_per_object);
69 Self {
70 name: name.into(),
71 render_path: BenchmarkRenderPath::PersistentSteps,
72 objects,
73 rotations_per_object: 1,
74 viewpoints_per_rotation: 0,
75 steps_per_object: Some(steps_per_object),
76 total_frames,
77 }
78 }
79}
80
81pub fn fixed_orbit_frame_count(
83 object_count: usize,
84 rotations_per_object: usize,
85 viewpoints_per_rotation: usize,
86) -> usize {
87 object_count
88 .saturating_mul(rotations_per_object)
89 .saturating_mul(viewpoints_per_rotation)
90}
91
92pub fn neocortx_targeting_policy(_rotation: &ObjectRotation) -> TargetingPolicy {
97 TargetingPolicy::MeshCenter
98}
99
100pub fn duration_ms(duration: Duration) -> f64 {
102 duration.as_secs_f64() * 1000.0
103}
104
105#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111pub struct TimingSummary {
112 pub total_frames: usize,
113 pub measurement_count: usize,
114 pub total_ms: f64,
115 pub frames_per_second: f64,
116 pub mean_ms_per_frame: f64,
117 pub p50_ms_per_frame: f64,
118 pub p95_ms_per_frame: f64,
119 pub min_ms_per_frame: f64,
120 pub max_ms_per_frame: f64,
121}
122
123impl TimingSummary {
124 pub fn empty() -> Self {
125 Self {
126 total_frames: 0,
127 measurement_count: 0,
128 total_ms: 0.0,
129 frames_per_second: 0.0,
130 mean_ms_per_frame: 0.0,
131 p50_ms_per_frame: 0.0,
132 p95_ms_per_frame: 0.0,
133 min_ms_per_frame: 0.0,
134 max_ms_per_frame: 0.0,
135 }
136 }
137}
138
139pub fn summarize_timing(
141 per_frame_ms_markers: &[f64],
142 total_frames: usize,
143 total_ms: f64,
144) -> TimingSummary {
145 if per_frame_ms_markers.is_empty() || total_frames == 0 {
146 return TimingSummary::empty();
147 }
148
149 let mean_ms_per_frame = total_ms / total_frames as f64;
150 let frames_per_second = if total_ms > 0.0 {
151 total_frames as f64 / (total_ms / 1000.0)
152 } else {
153 0.0
154 };
155
156 TimingSummary {
157 total_frames,
158 measurement_count: per_frame_ms_markers.len(),
159 total_ms,
160 frames_per_second,
161 mean_ms_per_frame,
162 p50_ms_per_frame: percentile(per_frame_ms_markers, 0.50).unwrap_or(0.0),
163 p95_ms_per_frame: percentile(per_frame_ms_markers, 0.95).unwrap_or(0.0),
164 min_ms_per_frame: per_frame_ms_markers
165 .iter()
166 .copied()
167 .fold(f64::INFINITY, f64::min),
168 max_ms_per_frame: per_frame_ms_markers
169 .iter()
170 .copied()
171 .fold(f64::NEG_INFINITY, f64::max),
172 }
173}
174
175pub fn percentile(samples: &[f64], quantile: f64) -> Option<f64> {
177 let mut values: Vec<f64> = samples
178 .iter()
179 .copied()
180 .filter(|value| value.is_finite())
181 .collect();
182 if values.is_empty() {
183 return None;
184 }
185
186 values.sort_by(|a, b| a.total_cmp(b));
187 let q = quantile.clamp(0.0, 1.0);
188 let rank = (q * (values.len().saturating_sub(1)) as f64).ceil() as usize;
189 values.get(rank).copied()
190}