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 if rotation.pitch.abs() > f64::EPSILON || rotation.roll.abs() > f64::EPSILON {
98 TargetingPolicy::MeshCenter
99 } else {
100 TargetingPolicy::Origin
101 }
102}
103
104pub fn duration_ms(duration: Duration) -> f64 {
106 duration.as_secs_f64() * 1000.0
107}
108
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
115pub struct TimingSummary {
116 pub total_frames: usize,
117 pub measurement_count: usize,
118 pub total_ms: f64,
119 pub frames_per_second: f64,
120 pub mean_ms_per_frame: f64,
121 pub p50_ms_per_frame: f64,
122 pub p95_ms_per_frame: f64,
123 pub min_ms_per_frame: f64,
124 pub max_ms_per_frame: f64,
125}
126
127impl TimingSummary {
128 pub fn empty() -> Self {
129 Self {
130 total_frames: 0,
131 measurement_count: 0,
132 total_ms: 0.0,
133 frames_per_second: 0.0,
134 mean_ms_per_frame: 0.0,
135 p50_ms_per_frame: 0.0,
136 p95_ms_per_frame: 0.0,
137 min_ms_per_frame: 0.0,
138 max_ms_per_frame: 0.0,
139 }
140 }
141}
142
143pub fn summarize_timing(
145 per_frame_ms_markers: &[f64],
146 total_frames: usize,
147 total_ms: f64,
148) -> TimingSummary {
149 if per_frame_ms_markers.is_empty() || total_frames == 0 {
150 return TimingSummary::empty();
151 }
152
153 let mean_ms_per_frame = total_ms / total_frames as f64;
154 let frames_per_second = if total_ms > 0.0 {
155 total_frames as f64 / (total_ms / 1000.0)
156 } else {
157 0.0
158 };
159
160 TimingSummary {
161 total_frames,
162 measurement_count: per_frame_ms_markers.len(),
163 total_ms,
164 frames_per_second,
165 mean_ms_per_frame,
166 p50_ms_per_frame: percentile(per_frame_ms_markers, 0.50).unwrap_or(0.0),
167 p95_ms_per_frame: percentile(per_frame_ms_markers, 0.95).unwrap_or(0.0),
168 min_ms_per_frame: per_frame_ms_markers
169 .iter()
170 .copied()
171 .fold(f64::INFINITY, f64::min),
172 max_ms_per_frame: per_frame_ms_markers
173 .iter()
174 .copied()
175 .fold(f64::NEG_INFINITY, f64::max),
176 }
177}
178
179pub fn percentile(samples: &[f64], quantile: f64) -> Option<f64> {
181 let mut values: Vec<f64> = samples
182 .iter()
183 .copied()
184 .filter(|value| value.is_finite())
185 .collect();
186 if values.is_empty() {
187 return None;
188 }
189
190 values.sort_by(|a, b| a.total_cmp(b));
191 let q = quantile.clamp(0.0, 1.0);
192 let rank = (q * (values.len().saturating_sub(1)) as f64).ceil() as usize;
193 values.get(rank).copied()
194}