Skip to main content

firewheel_graph/processor/
profiling.rs

1use 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    // Initialize the list of nodes with the graph output node.
30    #[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                    // Don't count the graph input node since it is processed separately.
140                    .filter(|node_id| *node_id != graph_in_node_id)
141                    .map(|node_id| NodeProfileData {
142                        node_id,
143                        // TODO: Try to re-use old cpu usage data.
144                        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/// Performance profiling information of a Firewheel Processor.
323#[derive(Default, Debug, Clone)]
324pub struct ProfilingData {
325    /// The number of times the profiling data has been updated.
326    pub version: u64,
327
328    /// The overall CPU usage of the entire Firewheel Processor's process method.
329    ///
330    /// A value of `0.0` means `0%` CPU usage, a value of `1.0` means `100%`
331    /// CPU usage of the total alloted time, and values above `1.0` means an
332    /// underrun has occurred.
333    ///
334    /// The value is the maximum value that has occurred since the last time
335    /// the profiling information was fetched with
336    /// [`FirewheelContext::profiling_data()`](crate::context::FirewheelContext::profiling_data).
337    pub overall_cpu_usage: f64,
338
339    /// The CPU usage of engine bookkeeping operations such as message handling,
340    /// event sorting, event searching, and final output processing.
341    ///
342    /// A value of `0.0` means `0%` CPU usage, and a value of `1.0` means `100%`
343    /// CPU usage of the total allotted time. (If instead you want the fraction
344    /// of time spent relative to the total time spent in the Firewheel process
345    /// method, it can be found with `bookkeeping_cpu_usage / overall_cpu_usage`.)
346    ///
347    /// The value is the maximum value that has occurred since the last time
348    /// the profiling information was fetched with
349    /// [`FirewheelContext::profiling_data()`](crate::context::FirewheelContext::profiling_data).
350    ///
351    /// If [`FirewheelFlags::profile_engine_bookkeeping`](crate::context::FirewheelFlags::profile_engine_bookkeeping)
352    /// is set to `false` (which it is by default), then this will be `None`.
353    pub engine_bookkeeping_cpu_usage: Option<f64>,
354
355    /// The profiling information of individual nodes.
356    ///
357    /// The order in which nodes appear is not defined.
358    ///
359    /// This may be empty if [`FirewheelFlags::profile_nodes`](crate::context::FirewheelFlags::profile_nodes)
360    /// is set to `false` (which it is by default) or if there was an error
361    /// with running out of buffer space.
362    #[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/// Performance profiling information of a Firewheel audio node.
390#[cfg(feature = "node_profiling")]
391#[derive(Default, Debug, Clone, Copy, PartialEq)]
392pub struct NodeProfileData {
393    /// The ID of the node.
394    pub node_id: NodeID,
395
396    /// The CPU usage of this node.
397    ///
398    /// A value of `0.0` means `0%` CPU usage, and a value of `1.0` means `100%`
399    /// CPU usage of the total allotted time. (If instead you want the fraction
400    /// of time spent relative to the total time spent in the Firewheel process
401    /// method, it can be found with `cpu_usage / ProfileData::overall_cpu_usage`.)
402    ///
403    /// The value is the maximum value that has occurred since the last time
404    /// the profiling information was fetched with
405    /// [`FirewheelContext::profile_info()`](crate::context::FirewheelContext::profile_info).
406    pub cpu_usage: f64,
407}