firewheel_graph/processor/
profiling.rs1use bevy_platform::time::Instant;
2
3#[cfg(all(feature = "node_profiling", not(feature = "std")))]
4use bevy_platform::prelude::Vec;
5
6use crate::context::FirewheelBitFlags;
7use crate::graph::CompiledSchedule;
8
9#[cfg(feature = "node_profiling")]
10use firewheel_core::node::NodeID;
11
12pub(crate) fn profiler_channel(
13 node_capacity: usize,
14 #[cfg(feature = "node_profiling")] graph_out_node_id: NodeID,
15) -> (ProfilerTx, ProfilerRx) {
16 let (buffer_tx, buffer_rx) =
17 triple_buffer::TripleBuffer::new(&ProfilingData::with_node_capacity(
18 #[cfg(feature = "node_profiling")]
19 node_capacity,
20 #[cfg(feature = "node_profiling")]
21 graph_out_node_id,
22 ))
23 .split();
24
25 let now = Instant::now();
26
27 #[allow(unused_mut)]
28 let mut heap_data = ProfilerHeapData::new(node_capacity, false);
29 #[cfg(feature = "node_profiling")]
31 heap_data.nodes.push(NodeProfileData {
32 node_id: graph_out_node_id,
33 cpu_usage: 0.0,
34 });
35
36 (
37 ProfilerTx {
38 buffer_tx,
39 version_counter: 0,
40 total_cpu_seconds_recip: 0.0,
41 proc_start_instant: now,
42 overall_cpu_usage: 0.0,
43 bookkeeping_cpu_usage: 0.0,
44 bookkeeping_cpu_usage_sum: 0.0,
45 is_profiling_bookkeeping: false,
46 bookkeeping_start_instant: now,
47 heap_data,
48 #[cfg(feature = "node_profiling")]
49 is_profiling_nodes: false,
50 #[cfg(feature = "node_profiling")]
51 node_profile_start_instant: now,
52 #[cfg(feature = "node_profiling")]
53 node_schedule_index: 0,
54 },
55 ProfilerRx { buffer_rx },
56 )
57}
58
59pub(crate) struct ProfilerTx {
60 buffer_tx: triple_buffer::Input<ProfilingData>,
61 version_counter: u64,
62 total_cpu_seconds_recip: f64,
63 proc_start_instant: Instant,
64
65 overall_cpu_usage: f64,
66 bookkeeping_cpu_usage: f64,
67 bookkeeping_cpu_usage_sum: f64,
68 is_profiling_bookkeeping: bool,
69 bookkeeping_start_instant: Instant,
70
71 heap_data: ProfilerHeapData,
72
73 #[cfg(feature = "node_profiling")]
74 is_profiling_nodes: bool,
75 #[cfg(feature = "node_profiling")]
76 node_profile_start_instant: Instant,
77 #[cfg(feature = "node_profiling")]
78 node_schedule_index: usize,
79}
80
81pub(crate) struct ProfilerHeapData {
82 #[cfg(feature = "node_profiling")]
83 nodes: Vec<NodeProfileData>,
84 #[cfg(feature = "node_profiling")]
85 node_cpu_sums: Vec<f64>,
86 #[cfg(feature = "node_profiling")]
87 triple_buf_allocations: [Vec<NodeProfileData>; 3],
88}
89
90impl ProfilerHeapData {
91 pub fn new(node_capacity: usize, triple_buf_allocations: bool) -> Self {
92 #[cfg(not(feature = "node_profiling"))]
93 let _ = node_capacity;
94 #[cfg(not(feature = "node_profiling"))]
95 let _ = triple_buf_allocations;
96
97 Self {
98 #[cfg(feature = "node_profiling")]
99 nodes: Vec::with_capacity(node_capacity),
100 #[cfg(feature = "node_profiling")]
101 node_cpu_sums: Vec::with_capacity(node_capacity),
102 #[cfg(feature = "node_profiling")]
103 triple_buf_allocations: if triple_buf_allocations {
104 [
105 Vec::with_capacity(node_capacity),
106 Vec::with_capacity(node_capacity),
107 Vec::with_capacity(node_capacity),
108 ]
109 } else {
110 [Vec::new(), Vec::new(), Vec::new()]
111 },
112 }
113 }
114}
115
116impl ProfilerTx {
117 pub fn new_schedule(
118 &mut self,
119 schedule: &CompiledSchedule,
120 new_heap_data: &mut Option<ProfilerHeapData>,
121 ) {
122 #[cfg(not(feature = "node_profiling"))]
123 let _ = schedule;
124
125 if let Some(new_heap_data) = new_heap_data.as_mut() {
126 core::mem::swap(&mut self.heap_data, new_heap_data);
127 }
128
129 #[cfg(feature = "node_profiling")]
130 {
131 self.heap_data.nodes.clear();
132 self.heap_data.node_cpu_sums.clear();
133
134 let graph_in_node_id = schedule.graph_in_node_id();
135
136 self.heap_data.nodes.extend(
137 schedule
138 .iter_node_ids()
139 .filter(|node_id| *node_id != graph_in_node_id)
141 .map(|node_id| NodeProfileData {
142 node_id,
143 cpu_usage: 0.0,
145 }),
146 );
147 }
148 }
149
150 pub fn new_process_loop(
151 &mut self,
152 proc_start_instant: Instant,
153 total_cpu_seconds_recip: f64,
154 flags: &FirewheelBitFlags,
155 ) {
156 self.proc_start_instant = proc_start_instant;
157 self.total_cpu_seconds_recip = total_cpu_seconds_recip;
158
159 self.bookkeeping_cpu_usage_sum = 0.0;
160 self.bookkeeping_start_instant = proc_start_instant;
161
162 let new_is_profiling_bookkeeping =
163 flags.contains(FirewheelBitFlags::PROFILE_ENGINE_BOOKKEEPING);
164 if new_is_profiling_bookkeeping && !self.is_profiling_bookkeeping {
165 self.bookkeeping_cpu_usage = 0.0;
166 }
167 self.is_profiling_bookkeeping = new_is_profiling_bookkeeping;
168
169 #[cfg(feature = "node_profiling")]
170 {
171 let new_is_profiling_nodes = flags.contains(FirewheelBitFlags::PROFILE_NODES);
172
173 if new_is_profiling_nodes && !self.is_profiling_nodes {
174 for node in self.heap_data.nodes.iter_mut() {
175 node.cpu_usage = 0.0;
176 }
177 }
178
179 self.is_profiling_nodes = new_is_profiling_nodes;
180
181 if self.is_profiling_nodes {
182 self.heap_data.node_cpu_sums.clear();
183 self.heap_data
184 .node_cpu_sums
185 .resize(self.heap_data.nodes.len(), 0.0);
186 }
187 }
188 }
189
190 pub fn begin_new_bookkeeping_part(&mut self) {
191 if self.is_profiling_bookkeeping
192 && let Some(now) = crate::time::now()
193 {
194 self.bookkeeping_start_instant = now;
195 }
196 }
197
198 pub fn bookkeeping_part_completed(&mut self) {
199 if self.is_profiling_bookkeeping {
200 let cpu_usage = self.bookkeeping_start_instant.elapsed().as_secs_f64()
201 * self.total_cpu_seconds_recip;
202 self.bookkeeping_cpu_usage_sum += cpu_usage;
203 }
204 }
205
206 #[cfg(feature = "node_profiling")]
207 pub fn begin_node_profiling(&mut self) {
208 self.node_schedule_index = 0;
209
210 if self.is_profiling_nodes
211 && let Some(now) = crate::time::now()
212 {
213 self.node_profile_start_instant = now;
214 }
215 }
216
217 #[cfg(feature = "node_profiling")]
218 pub fn node_completed(&mut self) {
219 if self.is_profiling_nodes
220 && let Some(new_profile_instant) = crate::time::now()
221 {
222 let node_cpu_usage = new_profile_instant
223 .duration_since(self.node_profile_start_instant)
224 .as_secs_f64()
225 * self.total_cpu_seconds_recip;
226 self.heap_data.node_cpu_sums[self.node_schedule_index] += node_cpu_usage;
227
228 self.node_profile_start_instant = new_profile_instant;
229 self.node_schedule_index += 1;
230 }
231 }
232
233 pub fn process_loop_completed(&mut self) {
234 let Some(now) = crate::time::now() else {
235 return;
236 };
237
238 #[cfg(feature = "node_profiling")]
239 if self.is_profiling_nodes {
240 for (node, &sum) in self
241 .heap_data
242 .nodes
243 .iter_mut()
244 .zip(self.heap_data.node_cpu_sums.iter())
245 {
246 node.cpu_usage = node.cpu_usage.max(sum);
247 }
248 }
249
250 let overall_cpu_usage = now.duration_since(self.proc_start_instant).as_secs_f64()
251 * self.total_cpu_seconds_recip;
252 self.overall_cpu_usage = self.overall_cpu_usage.max(overall_cpu_usage);
253
254 if self.is_profiling_bookkeeping {
255 let cpu_usage = now
256 .duration_since(self.bookkeeping_start_instant)
257 .as_secs_f64()
258 * self.total_cpu_seconds_recip;
259 self.bookkeeping_cpu_usage_sum += cpu_usage;
260
261 self.bookkeeping_cpu_usage = self
262 .bookkeeping_cpu_usage
263 .max(self.bookkeeping_cpu_usage_sum);
264 }
265
266 if self.buffer_tx.consumed() || self.version_counter == 0 {
267 {
268 let data = self.buffer_tx.input_buffer_mut();
269
270 data.version = self.version_counter;
271 data.overall_cpu_usage = self.overall_cpu_usage;
272
273 data.engine_bookkeeping_cpu_usage = self
274 .is_profiling_bookkeeping
275 .then_some(self.bookkeeping_cpu_usage);
276
277 #[cfg(feature = "node_profiling")]
278 if self.is_profiling_nodes {
279 if data.nodes.capacity() < self.heap_data.nodes.len()
280 && let Some(new_vec) = self
281 .heap_data
282 .triple_buf_allocations
283 .iter_mut()
284 .find(|v| v.capacity() >= self.heap_data.nodes.len())
285 {
286 core::mem::swap(&mut data.nodes, new_vec);
287 }
288
289 data.nodes.clear();
290 data.nodes.extend_from_slice(&self.heap_data.nodes);
291 } else {
292 data.nodes.clear();
293 }
294 }
295
296 self.buffer_tx.publish();
297 self.version_counter += 1;
298
299 self.overall_cpu_usage = 0.0;
300 self.bookkeeping_cpu_usage = 0.0;
301
302 #[cfg(feature = "node_profiling")]
303 if self.is_profiling_nodes {
304 for node in self.heap_data.nodes.iter_mut() {
305 node.cpu_usage = 0.0;
306 }
307 }
308 }
309 }
310}
311
312pub(crate) struct ProfilerRx {
313 buffer_rx: triple_buffer::Output<ProfilingData>,
314}
315
316impl ProfilerRx {
317 pub fn fetch_info(&mut self) -> &ProfilingData {
318 self.buffer_rx.read()
319 }
320}
321
322#[derive(Default, Debug, Clone)]
324pub struct ProfilingData {
325 pub version: u64,
327
328 pub overall_cpu_usage: f64,
338
339 pub engine_bookkeeping_cpu_usage: Option<f64>,
354
355 #[cfg(feature = "node_profiling")]
363 pub nodes: Vec<NodeProfileData>,
364}
365
366impl ProfilingData {
367 fn with_node_capacity(
368 #[cfg(feature = "node_profiling")] node_capacity: usize,
369 #[cfg(feature = "node_profiling")] graph_out_id: NodeID,
370 ) -> Self {
371 #[cfg(feature = "node_profiling")]
372 let mut nodes = Vec::with_capacity(node_capacity);
373 #[cfg(feature = "node_profiling")]
374 nodes.push(NodeProfileData {
375 node_id: graph_out_id,
376 cpu_usage: 0.0,
377 });
378
379 Self {
380 version: 0,
381 overall_cpu_usage: 0.0,
382 engine_bookkeeping_cpu_usage: None,
383 #[cfg(feature = "node_profiling")]
384 nodes,
385 }
386 }
387}
388
389#[cfg(feature = "node_profiling")]
391#[derive(Default, Debug, Clone, Copy, PartialEq)]
392pub struct NodeProfileData {
393 pub node_id: NodeID,
395
396 pub cpu_usage: f64,
407}