1use std::cell::Cell;
3
4#[derive(Clone, Copy, Debug, Default)]
5pub struct PhysicsWork {
6 pub steps: u64,
7 pub candidate_pairs: u64,
8 pub contact_points: u64,
9 pub ccd_impacts: u64,
10}
11thread_local! {static PHYSICS: Cell<PhysicsWork> = Cell::new(PhysicsWork::default());}
12pub fn record_physics(pairs: usize, contacts: usize, ccd: usize) {
14 PHYSICS.with(|cell| {
15 let mut v = cell.get();
16 v.steps += 1;
17 v.candidate_pairs += pairs as u64;
18 v.contact_points += contacts as u64;
19 v.ccd_impacts += ccd as u64;
20 cell.set(v);
21 });
22}
23pub fn take_physics() -> PhysicsWork {
24 PHYSICS.with(|cell| cell.replace(PhysicsWork::default()))
25}
26
27#[cfg(not(target_arch = "wasm32"))]
28mod recording {
29 use super::*;
30 use std::{
31 alloc::{GlobalAlloc, Layout, System},
32 path::PathBuf,
33 sync::atomic::{AtomicBool, AtomicU64, Ordering},
34 time::Instant,
35 };
36 static ENABLED: AtomicBool = AtomicBool::new(false);
37 static CALLS: AtomicU64 = AtomicU64::new(0);
38 static BYTES: AtomicU64 = AtomicU64::new(0);
39 pub struct TrackingAllocator;
42 fn allocated(ptr: *mut u8, bytes: usize) {
43 if !ptr.is_null() && ENABLED.load(Ordering::Relaxed) {
44 CALLS.fetch_add(1, Ordering::Relaxed);
45 BYTES.fetch_add(bytes as u64, Ordering::Relaxed);
46 }
47 }
48 unsafe impl GlobalAlloc for TrackingAllocator {
50 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
51 let p = unsafe { System.alloc(layout) };
52 allocated(p, layout.size());
53 p
54 }
55 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
56 let p = unsafe { System.alloc_zeroed(layout) };
57 allocated(p, layout.size());
58 p
59 }
60 unsafe fn realloc(&self, p: *mut u8, layout: Layout, size: usize) -> *mut u8 {
61 let p = unsafe { System.realloc(p, layout, size) };
62 allocated(p, size);
63 p
64 }
65 unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
66 unsafe { System.dealloc(p, layout) };
67 }
68 }
69 #[derive(Clone, Debug, Default)]
70 pub struct FrameSample {
71 pub cpu_us: f64,
72 pub submit_us: Option<f64>,
74 pub software_render_us: Option<f64>,
75 pub gpu_us: Option<f64>,
76 pub draw_calls: Option<u64>,
77 pub vertices: Option<u64>,
78 pub upload_bytes: Option<u64>,
79 pub allocation_calls: u64,
80 pub allocation_bytes: u64,
81 pub physics: PhysicsWork,
82 }
83 pub struct FrameToken {
84 start: Instant,
85 calls: u64,
86 bytes: u64,
87 }
88 impl FrameToken {
89 pub fn begin() -> Self {
90 take_physics();
91 Self {
92 start: Instant::now(),
93 calls: CALLS.load(Ordering::Relaxed),
94 bytes: BYTES.load(Ordering::Relaxed),
95 }
96 }
97 pub fn elapsed_us(&self) -> f64 {
98 self.start.elapsed().as_secs_f64() * 1e6
99 }
100 pub fn finish(self, mut sample: FrameSample) -> FrameSample {
101 sample.allocation_calls = CALLS.load(Ordering::Relaxed).saturating_sub(self.calls);
102 sample.allocation_bytes = BYTES.load(Ordering::Relaxed).saturating_sub(self.bytes);
103 sample.physics = take_physics();
104 sample
105 }
106 }
107 pub struct Session {
109 output: PathBuf,
110 warmup: usize,
111 seen: usize,
112 dropped: usize,
113 samples: Vec<FrameSample>,
114 gpu_source: Option<&'static str>,
115 render_quality: Option<u32>,
116 }
117 impl Session {
118 pub fn from_env() -> Option<Self> {
119 let output = std::env::var_os("FERRAVEX_PERF_OUTPUT")?;
120 let warmup = std::env::var("FERRAVEX_PERF_WARMUP")
121 .map_or(60, |s| {
122 s.parse::<usize>().expect("invalid FERRAVEX_PERF_WARMUP")
123 })
124 .min(100_000);
125 let session = Self {
126 output: output.into(),
127 warmup,
128 seen: 0,
129 dropped: 0,
130 samples: Vec::with_capacity(8192),
131 gpu_source: None,
132 render_quality: None,
133 };
134 ENABLED.store(true, Ordering::Relaxed);
135 Some(session)
136 }
137 pub fn record(&mut self, sample: FrameSample) {
138 self.seen += 1;
139 if self.seen <= self.warmup {
140 return;
141 }
142 if self.samples.len() < self.samples.capacity() {
143 self.samples.push(sample);
144 } else {
145 self.dropped += 1;
146 }
147 }
148 pub fn render_quality(&mut self, quality: u32) {
149 self.render_quality = Some(quality);
150 }
151 pub fn gpu_source(&mut self, source: &'static str) {
152 self.gpu_source = Some(source);
153 }
154 pub fn gpu_result(&mut self, frame: u64, microseconds: f64) {
156 if !microseconds.is_finite() || microseconds < 0. || frame <= self.warmup as u64 {
157 return;
158 }
159 let index = frame - self.warmup as u64 - 1;
160 if let Some(sample) = self.samples.get_mut(index as usize) {
161 sample.gpu_us = Some(microseconds);
162 }
163 }
164 pub fn write(&self, backend: &str, width: u32, height: u32) -> std::io::Result<()> {
165 let mut out = format!(
166 "{{\"schema\":2,\"backend\":{},\"os\":{},\"arch\":{},\"width\":{width},\"height\":{height},\"warmup\":{},\"dropped\":{},\"gpu_source\":{},\"render_quality\":{},\"frames\":[",
167 json_string(backend),
168 json_string(std::env::consts::OS),
169 json_string(std::env::consts::ARCH),
170 self.warmup,
171 self.dropped,
172 self.gpu_source.map_or_else(|| "null".into(), json_string),
173 optional(self.render_quality)
174 );
175 for (i, s) in self.samples.iter().enumerate() {
176 if i > 0 {
177 out.push(',');
178 }
179 out.push_str(&format!("{{\"cpu_us\":{},\"submit_us\":{},\"software_render_us\":{},\"draw_calls\":{},\"vertices\":{},\"upload_bytes\":{},\"allocation_calls\":{},\"allocation_bytes\":{},\"physics_steps\":{},\"physics_pairs\":{},\"contact_points\":{},\"ccd_impacts\":{},\"gpu_us\":{}}}",s.cpu_us,optional(s.submit_us),optional(s.software_render_us),optional(s.draw_calls),optional(s.vertices),optional(s.upload_bytes),s.allocation_calls,s.allocation_bytes,s.physics.steps,s.physics.candidate_pairs,s.physics.contact_points,s.physics.ccd_impacts,optional(s.gpu_us)));
180 }
181 out.push_str("]}\n");
182 std::fs::write(&self.output, out)
183 }
184 }
185 fn optional<T: std::fmt::Display>(v: Option<T>) -> String {
186 v.map_or_else(|| "null".into(), |n| n.to_string())
187 }
188 pub fn json_string(value: &str) -> String {
189 let mut out = String::from("\"");
190 for c in value.chars() {
191 match c {
192 '"' => out.push_str("\\\""),
193 '\\' => out.push_str("\\\\"),
194 c if c < ' ' => out.push_str(&format!("\\u{:04x}", c as u32)),
195 c => out.push(c),
196 }
197 }
198 out.push('"');
199 out
200 }
201 #[cfg(test)]
202 mod tests {
203 use super::*;
204 #[test]
205 fn counters_sum_catchup_ticks_and_reset() {
206 take_physics();
207 record_physics(3, 4, 1);
208 record_physics(2, 5, 0);
209 let v = take_physics();
210 assert_eq!(
211 (v.steps, v.candidate_pairs, v.contact_points, v.ccd_impacts),
212 (2, 5, 9, 1)
213 );
214 assert_eq!(take_physics().steps, 0);
215 }
216 #[test]
217 fn strings_are_json_safe() {
218 assert_eq!(json_string("a\n\"\\\0"), "\"a\\u000a\\\"\\\\\\u0000\"");
219 }
220 #[test]
221 fn warmup_and_capacity_are_explicit() {
222 let mut s = Session {
223 output: PathBuf::new(),
224 warmup: 2,
225 seen: 0,
226 dropped: 0,
227 samples: Vec::with_capacity(2),
228 gpu_source: None,
229 render_quality: None,
230 };
231 for _ in 0..5 {
232 s.record(FrameSample::default());
233 }
234 assert_eq!((s.samples.len(), s.dropped), (2, 1));
235 s.gpu_result(2, 999.); s.gpu_result(4, 17.);
237 s.gpu_result(3, 12.);
238 s.gpu_result(3, f64::NAN);
239 s.gpu_result(4, -1.);
240 s.gpu_result(8, 4.); assert_eq!(s.samples[0].gpu_us, Some(12.));
242 assert_eq!(s.samples[1].gpu_us, Some(17.));
243 }
244 }
245}
246#[cfg(not(target_arch = "wasm32"))]
247pub use recording::*;