diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa2c01ba..8d943093 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - name: Check no_std build run: | - cargo build --locked --no-default-features --features "libm, scheduled_events, musical_transport, all_nodes_no_std, pool, glam-29, glam-30, glam-31" + cargo build --locked --no-default-features --features "libm, scheduled_events, musical_transport, all_nodes_no_std, pool, node_profiling, glam-29, glam-30, glam-31" # Check formatting. format: diff --git a/crates/firewheel-graph/src/context.rs b/crates/firewheel-graph/src/context.rs index 9a83fd8d..9cf1bac1 100644 --- a/crates/firewheel-graph/src/context.rs +++ b/crates/firewheel-graph/src/context.rs @@ -347,12 +347,7 @@ impl FirewheelContext { let (logger, logger_rx) = firewheel_core::log::realtime_logger(config.logger_config); let (profiler_tx, profiler_rx) = crate::processor::profiling::profiler_channel( - #[cfg(feature = "node_profiling")] - { - config.initial_node_capacity as usize - }, - #[cfg(feature = "node_profiling")] - config.buffer_out_of_space_mode, + config.initial_node_capacity as usize, #[cfg(feature = "node_profiling")] graph.graph_out_node(), ); diff --git a/crates/firewheel-graph/src/graph.rs b/crates/firewheel-graph/src/graph.rs index 89f8fa21..5fdff5a4 100644 --- a/crates/firewheel-graph/src/graph.rs +++ b/crates/firewheel-graph/src/graph.rs @@ -18,6 +18,7 @@ use thunderdome::Arena; use crate::FirewheelConfig; use crate::error::{AddEdgeError, CompileGraphError, RemoveNodeError}; use crate::graph::dummy_node::{DummyNode, DummyNodeConfig}; +use crate::processor::profiling::ProfilerHeapData; use firewheel_core::node::{ AudioNode, AudioNodeInfo, AudioNodeInfoInner, Constructor, DynAudioNode, NodeID, }; @@ -712,11 +713,15 @@ impl AudioGraph { &mut nodes_to_remove, ); - let new_arena = if self.nodes.capacity() > self.prev_node_arena_capacity { - Some(Arena::with_capacity(self.nodes.capacity())) - } else { - None - }; + let (new_arena, new_profiler_heap_data) = + if self.nodes.capacity() > self.prev_node_arena_capacity { + ( + Some(Arena::with_capacity(self.nodes.capacity())), + Some(ProfilerHeapData::new(self.nodes.capacity(), true)), + ) + } else { + (None, None) + }; self.prev_node_arena_capacity = self.nodes.capacity(); let schedule_data = Box::new(ScheduleHeapData::new( @@ -724,6 +729,7 @@ impl AudioGraph { nodes_to_remove, new_node_processors, new_arena, + new_profiler_heap_data, )); self.needs_compile = false; diff --git a/crates/firewheel-graph/src/graph/compiler/schedule.rs b/crates/firewheel-graph/src/graph/compiler/schedule.rs index 09b6dde9..2308a992 100644 --- a/crates/firewheel-graph/src/graph/compiler/schedule.rs +++ b/crates/firewheel-graph/src/graph/compiler/schedule.rs @@ -11,6 +11,8 @@ use firewheel_core::{ node::{AudioNodeProcessor, ProcBuffers, ProcessStatus}, }; +use crate::processor::profiling::ProfilerHeapData; + use super::{InsertedSum, NodeID}; #[cfg(not(feature = "std"))] @@ -183,6 +185,7 @@ pub struct ScheduleHeapData { pub(crate) removed_nodes: Vec, pub(crate) new_node_processors: Vec, pub(crate) new_node_arena: Option>, + pub(crate) new_profiler_heap_data: Option, } impl ScheduleHeapData { @@ -191,6 +194,7 @@ impl ScheduleHeapData { nodes_to_remove: Vec, new_node_processors: Vec, new_node_arena: Option>, + new_profiler_heap_data: Option, ) -> Self { let num_nodes_to_remove = nodes_to_remove.len(); @@ -200,6 +204,7 @@ impl ScheduleHeapData { removed_nodes: Vec::with_capacity(num_nodes_to_remove), new_node_processors, new_node_arena, + new_profiler_heap_data, } } } @@ -358,11 +363,6 @@ impl CompiledSchedule { } } - #[cfg(feature = "node_profiling")] - pub(crate) fn num_nodes(&self) -> usize { - self.pre_proc_nodes.len() + self.schedule.len() - } - pub(crate) fn buffer_capacity(&self) -> usize { self.buffer_capacity } diff --git a/crates/firewheel-graph/src/processor/handle_messages.rs b/crates/firewheel-graph/src/processor/handle_messages.rs index 7af1658d..573fa85e 100644 --- a/crates/firewheel-graph/src/processor/handle_messages.rs +++ b/crates/firewheel-graph/src/processor/handle_messages.rs @@ -143,8 +143,10 @@ impl FirewheelProcessorInner { .remove_events_from_removed_nodes(&self.nodes); } - self.profiler_tx - .new_schedule(&new_schedule_data.schedule, &mut self.extra.logger); + self.profiler_tx.new_schedule( + &new_schedule_data.schedule, + &mut new_schedule_data.new_profiler_heap_data, + ); self.schedule_data = Some(new_schedule_data); } diff --git a/crates/firewheel-graph/src/processor/profiling.rs b/crates/firewheel-graph/src/processor/profiling.rs index 67ea2d14..a22adfe3 100644 --- a/crates/firewheel-graph/src/processor/profiling.rs +++ b/crates/firewheel-graph/src/processor/profiling.rs @@ -1,34 +1,34 @@ -use firewheel_core::log::RealtimeLogger; - use bevy_platform::time::Instant; +#[cfg(not(feature = "std"))] +use bevy_platform::prelude::Vec; + use crate::context::FirewheelBitFlags; use crate::graph::CompiledSchedule; -#[cfg(feature = "node_profiling")] -use crate::processor::BufferOutOfSpaceMode; #[cfg(feature = "node_profiling")] use firewheel_core::node::NodeID; pub(crate) fn profiler_channel( - #[cfg(feature = "node_profiling")] node_capacity: usize, - #[cfg(feature = "node_profiling")] buffer_out_of_space_mode: BufferOutOfSpaceMode, + node_capacity: usize, #[cfg(feature = "node_profiling")] graph_out_node_id: NodeID, ) -> (ProfilerTx, ProfilerRx) { let (buffer_tx, buffer_rx) = triple_buffer::TripleBuffer::new(&ProfilingData::with_node_capacity( #[cfg(feature = "node_profiling")] node_capacity, + #[cfg(feature = "node_profiling")] + graph_out_node_id, )) .split(); let now = Instant::now(); - #[cfg(feature = "node_profiling")] - let mut nodes = Vec::with_capacity(node_capacity); - #[cfg(feature = "node_profiling")] + #[allow(unused_mut)] + let mut heap_data = ProfilerHeapData::new(node_capacity, false); // Initialize the list of nodes with the graph output node. - nodes.push(NodeProfileData { + #[cfg(feature = "node_profiling")] + heap_data.nodes.push(NodeProfileData { node_id: graph_out_node_id, cpu_usage: 0.0, }); @@ -44,19 +44,10 @@ pub(crate) fn profiler_channel( bookkeeping_cpu_usage_sum: 0.0, is_profiling_bookkeeping: false, bookkeeping_start_instant: now, - #[cfg(feature = "node_profiling")] - nodes, - #[cfg(feature = "node_profiling")] - node_cpu_sums: Vec::with_capacity(node_capacity), - #[cfg(feature = "node_profiling")] - node_capacity, - #[cfg(feature = "node_profiling")] - has_enough_node_capacity: true, + heap_data, #[cfg(feature = "node_profiling")] is_profiling_nodes: false, #[cfg(feature = "node_profiling")] - buffer_out_of_space_mode, - #[cfg(feature = "node_profiling")] node_profile_start_instant: now, #[cfg(feature = "node_profiling")] node_schedule_index: 0, @@ -77,80 +68,82 @@ pub(crate) struct ProfilerTx { is_profiling_bookkeeping: bool, bookkeeping_start_instant: Instant, - #[cfg(feature = "node_profiling")] - nodes: Vec, - #[cfg(feature = "node_profiling")] - node_cpu_sums: Vec, - #[cfg(feature = "node_profiling")] - node_capacity: usize, - #[cfg(feature = "node_profiling")] - has_enough_node_capacity: bool, + heap_data: ProfilerHeapData, + #[cfg(feature = "node_profiling")] is_profiling_nodes: bool, #[cfg(feature = "node_profiling")] - buffer_out_of_space_mode: BufferOutOfSpaceMode, - #[cfg(feature = "node_profiling")] node_profile_start_instant: Instant, #[cfg(feature = "node_profiling")] node_schedule_index: usize, } +pub(crate) struct ProfilerHeapData { + #[cfg(feature = "node_profiling")] + nodes: Vec, + #[cfg(feature = "node_profiling")] + node_cpu_sums: Vec, + #[cfg(feature = "node_profiling")] + triple_buf_allocations: [Vec; 3], +} + +impl ProfilerHeapData { + pub fn new(node_capacity: usize, triple_buf_allocations: bool) -> Self { + #[cfg(not(feature = "node_profiling"))] + let _ = node_capacity; + #[cfg(not(feature = "node_profiling"))] + let _ = triple_buf_allocations; + + Self { + #[cfg(feature = "node_profiling")] + nodes: Vec::with_capacity(node_capacity), + #[cfg(feature = "node_profiling")] + node_cpu_sums: Vec::with_capacity(node_capacity), + #[cfg(feature = "node_profiling")] + triple_buf_allocations: if triple_buf_allocations { + [ + Vec::with_capacity(node_capacity), + Vec::with_capacity(node_capacity), + Vec::with_capacity(node_capacity), + ] + } else { + [Vec::new(), Vec::new(), Vec::new()] + }, + } + } +} + impl ProfilerTx { - pub fn new_schedule(&mut self, schedule: &CompiledSchedule, logger: &mut RealtimeLogger) { + pub fn new_schedule( + &mut self, + schedule: &CompiledSchedule, + new_heap_data: &mut Option, + ) { #[cfg(not(feature = "node_profiling"))] - { - let _ = schedule; - let _ = logger; + let _ = schedule; + + if let Some(new_heap_data) = new_heap_data.as_mut() { + core::mem::swap(&mut self.heap_data, new_heap_data); } #[cfg(feature = "node_profiling")] { - let num_nodes = schedule.num_nodes(); - - // TODO: Try to re-use old data. - self.nodes.clear(); - self.node_cpu_sums.clear(); - - if self.node_capacity < num_nodes { - // TODO: A new Vec should just be sent via ScheduleHeapData instead of dealing - // with buffer out of space logic. - match self.buffer_out_of_space_mode { - BufferOutOfSpaceMode::AllocateOnAudioThread => { - let _ = logger.try_error("Firewheel node profiling buffer is full! Please increase FirewheelConfig::initial_node_capacity to avoid audio glitches."); - - self.node_capacity = (num_nodes * 2).next_power_of_two(); - self.nodes.reserve(self.node_capacity); - self.node_cpu_sums.reserve(self.node_capacity); - self.has_enough_node_capacity = true; - } - BufferOutOfSpaceMode::Panic => { - panic!( - "Firewheel node profiling buffer is full! Please increase FirewheelConfig::initial_node_capacity." - ); - } - BufferOutOfSpaceMode::DropEvents => { - let _ = logger.try_error("Firewheel node profiling buffer is full! Please increase FirewheelConfig::initial_node_capacity."); - self.has_enough_node_capacity = false; - } - } - } else { - self.has_enough_node_capacity = true; - } - - if self.has_enough_node_capacity { - let graph_in_node_id = schedule.graph_in_node_id(); - - self.nodes.extend( - schedule - .iter_node_ids() - // Don't count the graph input node since it is processed separately. - .filter(|node_id| *node_id != graph_in_node_id) - .map(|node_id| NodeProfileData { - node_id, - cpu_usage: 0.0, - }), - ); - } + self.heap_data.nodes.clear(); + self.heap_data.node_cpu_sums.clear(); + + let graph_in_node_id = schedule.graph_in_node_id(); + + self.heap_data.nodes.extend( + schedule + .iter_node_ids() + // Don't count the graph input node since it is processed separately. + .filter(|node_id| *node_id != graph_in_node_id) + .map(|node_id| NodeProfileData { + node_id, + // TODO: Try to re-use old cpu usage data. + cpu_usage: 0.0, + }), + ); } } @@ -175,11 +168,10 @@ impl ProfilerTx { #[cfg(feature = "node_profiling")] { - let new_is_profiling_nodes = - flags.contains(FirewheelBitFlags::PROFILE_NODES) && self.has_enough_node_capacity; + let new_is_profiling_nodes = flags.contains(FirewheelBitFlags::PROFILE_NODES); if new_is_profiling_nodes && !self.is_profiling_nodes { - for node in self.nodes.iter_mut() { + for node in self.heap_data.nodes.iter_mut() { node.cpu_usage = 0.0; } } @@ -187,8 +179,10 @@ impl ProfilerTx { self.is_profiling_nodes = new_is_profiling_nodes; if self.is_profiling_nodes { - self.node_cpu_sums.clear(); - self.node_cpu_sums.resize(self.nodes.len(), 0.0); + self.heap_data.node_cpu_sums.clear(); + self.heap_data + .node_cpu_sums + .resize(self.heap_data.nodes.len(), 0.0); } } } @@ -229,7 +223,7 @@ impl ProfilerTx { .duration_since(self.node_profile_start_instant) .as_secs_f64() * self.total_cpu_seconds_recip; - self.node_cpu_sums[self.node_schedule_index] += node_cpu_usage; + self.heap_data.node_cpu_sums[self.node_schedule_index] += node_cpu_usage; self.node_profile_start_instant = new_profile_instant; self.node_schedule_index += 1; @@ -243,7 +237,12 @@ impl ProfilerTx { #[cfg(feature = "node_profiling")] if self.is_profiling_nodes { - for (node, &sum) in self.nodes.iter_mut().zip(self.node_cpu_sums.iter()) { + for (node, &sum) in self + .heap_data + .nodes + .iter_mut() + .zip(self.heap_data.node_cpu_sums.iter()) + { node.cpu_usage = node.cpu_usage.max(sum); } } @@ -275,13 +274,22 @@ impl ProfilerTx { .is_profiling_bookkeeping .then_some(self.bookkeeping_cpu_usage); - #[cfg(feature = "node_profiling")] - data.nodes.clear(); - #[cfg(feature = "node_profiling")] if self.is_profiling_nodes { - data.nodes.reserve(self.node_capacity); - data.nodes.extend_from_slice(&self.nodes); + if data.nodes.capacity() < self.heap_data.nodes.len() + && let Some(new_vec) = self + .heap_data + .triple_buf_allocations + .iter_mut() + .find(|v| v.capacity() >= self.heap_data.nodes.len()) + { + core::mem::swap(&mut data.nodes, new_vec); + } + + data.nodes.clear(); + data.nodes.extend_from_slice(&self.heap_data.nodes); + } else { + data.nodes.clear(); } } @@ -293,7 +301,7 @@ impl ProfilerTx { #[cfg(feature = "node_profiling")] if self.is_profiling_nodes { - for node in self.nodes.iter_mut() { + for node in self.heap_data.nodes.iter_mut() { node.cpu_usage = 0.0; } } @@ -356,13 +364,24 @@ pub struct ProfilingData { } impl ProfilingData { - fn with_node_capacity(#[cfg(feature = "node_profiling")] node_capacity: usize) -> Self { + fn with_node_capacity( + #[cfg(feature = "node_profiling")] node_capacity: usize, + #[cfg(feature = "node_profiling")] graph_out_id: NodeID, + ) -> Self { + #[cfg(feature = "node_profiling")] + let mut nodes = Vec::with_capacity(node_capacity); + #[cfg(feature = "node_profiling")] + nodes.push(NodeProfileData { + node_id: graph_out_id, + cpu_usage: 0.0, + }); + Self { version: 0, overall_cpu_usage: 0.0, engine_bookkeeping_cpu_usage: None, #[cfg(feature = "node_profiling")] - nodes: vec![NodeProfileData::default(); node_capacity], + nodes, } } }