feat: implement real-time DSP latency profiler and GUI dashboard (#117)#121
Conversation
…tants LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
📝 WalkthroughWalkthroughThe PR instruments AudioEngine to record per-effect processing time and exposes accessors; adds a GuiManager profiler window showing total callback time, CPU load, and per-pedal timings with progress bars; and adds a Utilities menu item to toggle the profiler window. ChangesDSP Performance Profiler Feature
Estimated code review effort: Possibly related PRs:
Suggested labels: Suggested reviewers:
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gui/gui_manager_frame.cpp`:
- Around line 254-271: The loop that renders pedal profiling rows iterates over
all effects (effects.size()) but engine_.get_effect_process_time_us only records
the first 32 slots, causing misleading zeros; change the loop to iterate only up
to the profiled range (e.g., min(effects.size(), 32) or the engine's
MAX_PROFILE_SLOTS constant) when calling get_effect_process_time_us and
rendering the ProgressBar, and after the loop render a single informational row
or Text note indicating how many additional pedals are not profiled so users
know timings are only available for the profiled range.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bf8702a3-3a77-43c3-aa84-d7222d70bb11
📒 Files selected for processing (5)
src/audio/audio_engine.hsrc/audio/audio_engine_process.cppsrc/gui/gui_manager.hsrc/gui/gui_manager_frame.cppsrc/gui/gui_manager_menu.cpp
LiveReview Pre-Commit Check: ran (iter:2, coverage:35%)
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/audio/audio_engine_process.cpp`:
- Around line 103-105: Restore real draining in AudioEngine::drain_commands():
grab the command-queue under its mutex, loop draining each queued command and
apply it on the audio thread instead of returning early — for each item call the
appropriate apply/execute handler (e.g. parameter-update handler,
enable/mix-change handler, topology-edit handler or the command object's
apply/process method) and remove it from the queue, then release the lock;
ensure you use the existing queue and mutex member names (e.g. queued_commands /
command_queue and command_queue_mutex or their actual equivalents) and clear any
processed commands so queued state changes are executed on the audio thread.
- Around line 29-32: The loop writes into process_buffer_ and
process_buffer_right_ without ensuring they're large enough for frame_count,
risking OOB writes; before the for-loop in the audio callback (where
process_buffer_, process_buffer_right_, frame_count and input are used) check
the current sizes and grow them if needed (e.g., if process_buffer_.size() <
frame_count or process_buffer_right_.size() < frame_count then resize to
frame_count) so indexing is always safe—perform the minimal check/resize prior
to the loop to guard the scratch buffers.
- Around line 71-76: The current else branch for when analyzer_capture_index_ +
frame_count > ANALYZER_FFT_SIZE drops the incoming block and fails when
frame_count >= ANALYZER_FFT_SIZE; replace it with a wrap-safe copy into
analyzer_capture_input_: compute space_remaining = ANALYZER_FFT_SIZE -
analyzer_capture_index_; if frame_count < ANALYZER_FFT_SIZE and frame_count >
space_remaining, memcpy the first space_remaining samples from process_buffer_
to analyzer_capture_input_ at analyzer_capture_index_, then memcpy the remaining
samples to analyzer_capture_input_ at offset 0, and set analyzer_capture_index_
= (analyzer_capture_index_ + frame_count) % ANALYZER_FFT_SIZE; if frame_count >=
ANALYZER_FFT_SIZE, copy only the last ANALYZER_FFT_SIZE samples from
process_buffer_ into analyzer_capture_input_ (starting at 0) and set
analyzer_capture_index_ = ANALYZER_FFT_SIZE (buffer full). Ensure all memcpy
sizes use static_cast<size_t>(...) * sizeof(float) and reference
process_buffer_, analyzer_capture_input_, analyzer_capture_index_, frame_count,
and ANALYZER_FFT_SIZE.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0354e330-7dd8-498f-b7c0-3a14f03cae4f
📒 Files selected for processing (2)
src/audio/audio_engine_process.cppsrc/gui/gui_manager_frame.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
- src/gui/gui_manager_frame.cpp
| for (int i = 0; i < frame_count; ++i) { | ||
| process_buffer_[i] = input[2 * i] * in_gain; | ||
| process_buffer_right_[i] = input[2 * i + 1] * in_gain; | ||
| } |
There was a problem hiding this comment.
Guard the scratch buffers before indexing them.
These writes assume process_buffer_ and process_buffer_right_ are already at least frame_count long. If the callback ever arrives with a larger block than the current allocation, this becomes an out-of-bounds write on the audio thread.
Suggested minimal guard
+ const auto required_frames = static_cast<size_t>(frame_count);
+ if (process_buffer_.size() < required_frames ||
+ process_buffer_right_.size() < required_frames) {
+ std::memset(output, 0, required_frames * 2 * sizeof(float));
+ return;
+ }
+
float in_gain = input_gain_.load(std::memory_order_relaxed);
for (int i = 0; i < frame_count; ++i) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/audio/audio_engine_process.cpp` around lines 29 - 32, The loop writes
into process_buffer_ and process_buffer_right_ without ensuring they're large
enough for frame_count, risking OOB writes; before the for-loop in the audio
callback (where process_buffer_, process_buffer_right_, frame_count and input
are used) check the current sizes and grow them if needed (e.g., if
process_buffer_.size() < frame_count or process_buffer_right_.size() <
frame_count then resize to frame_count) so indexing is always safe—perform the
minimal check/resize prior to the loop to guard the scratch buffers.
| if (analyzer_capture_index_ + frame_count <= ANALYZER_FFT_SIZE) { | ||
| std::memcpy(&analyzer_capture_input_[analyzer_capture_index_], process_buffer_.data(), | ||
| static_cast<size_t>(frame_count) * sizeof(float)); | ||
| analyzer_capture_index_ += frame_count; | ||
| } else { | ||
| for (int i = 0; i < frame_count; ++i) { | ||
| float out_l = clamp(process_buffer_[i] * out_gain, -1.0f, 1.0f); | ||
| float out_r = clamp(process_buffer_right_[i] * out_gain, -1.0f, 1.0f); | ||
| output[i * 2] = out_l; | ||
| output[i * 2 + 1] = out_r; | ||
| process_buffer_[i] = out_l; | ||
| float abs_val = std::fabs(out_l); | ||
| if (abs_val > peak_out) peak_out = abs_val; | ||
| } | ||
| analyzer_capture_index_ = 0; |
There was a problem hiding this comment.
Don’t drop the analyzer block on wrap.
When the capture window fills, the else path resets analyzer_capture_index_ and discards the current block entirely. That introduces gaps in the FFT input, and if frame_count is ever larger than ANALYZER_FFT_SIZE, this path never copies anything.
Suggested wrap-safe copy
- if (analyzer_capture_index_ + frame_count <= ANALYZER_FFT_SIZE) {
- std::memcpy(&analyzer_capture_input_[analyzer_capture_index_], process_buffer_.data(),
- static_cast<size_t>(frame_count) * sizeof(float));
- analyzer_capture_index_ += frame_count;
- } else {
- analyzer_capture_index_ = 0;
- }
+ const size_t copy_frames = std::min(
+ static_cast<size_t>(frame_count),
+ static_cast<size_t>(ANALYZER_FFT_SIZE));
+
+ if (analyzer_capture_index_ + static_cast<int>(copy_frames) > ANALYZER_FFT_SIZE) {
+ analyzer_capture_index_ = 0;
+ }
+
+ std::memcpy(&analyzer_capture_input_[analyzer_capture_index_],
+ process_buffer_.data(),
+ copy_frames * sizeof(float));
+ analyzer_capture_index_ += static_cast<int>(copy_frames);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/audio/audio_engine_process.cpp` around lines 71 - 76, The current else
branch for when analyzer_capture_index_ + frame_count > ANALYZER_FFT_SIZE drops
the incoming block and fails when frame_count >= ANALYZER_FFT_SIZE; replace it
with a wrap-safe copy into analyzer_capture_input_: compute space_remaining =
ANALYZER_FFT_SIZE - analyzer_capture_index_; if frame_count < ANALYZER_FFT_SIZE
and frame_count > space_remaining, memcpy the first space_remaining samples from
process_buffer_ to analyzer_capture_input_ at analyzer_capture_index_, then
memcpy the remaining samples to analyzer_capture_input_ at offset 0, and set
analyzer_capture_index_ = (analyzer_capture_index_ + frame_count) %
ANALYZER_FFT_SIZE; if frame_count >= ANALYZER_FFT_SIZE, copy only the last
ANALYZER_FFT_SIZE samples from process_buffer_ into analyzer_capture_input_
(starting at 0) and set analyzer_capture_index_ = ANALYZER_FFT_SIZE (buffer
full). Ensure all memcpy sizes use static_cast<size_t>(...) * sizeof(float) and
reference process_buffer_, analyzer_capture_input_, analyzer_capture_index_,
frame_count, and ANALYZER_FFT_SIZE.
| void AudioEngine::drain_commands() { | ||
| AudioCommand cmd; | ||
| while (command_queue_.try_pop(cmd)) { | ||
| switch (cmd.type) { | ||
| case AudioCommand::SetEffectParam: | ||
| if (cmd.effect_index >= 0 && | ||
| cmd.effect_index < static_cast<int>(effects_.size())) { | ||
| auto& params = effects_[cmd.effect_index]->params(); | ||
| if (cmd.param_index >= 0 && | ||
| cmd.param_index < static_cast<int>(params.size())) { | ||
| params[cmd.param_index].value = cmd.value; | ||
| } | ||
| } | ||
| break; | ||
| case AudioCommand::SetEffectEnabled: | ||
| if (cmd.effect_index >= 0 && | ||
| cmd.effect_index < static_cast<int>(effects_.size())) { | ||
| effects_[cmd.effect_index]->set_enabled(cmd.value > 0.5f); | ||
| } | ||
| break; | ||
| case AudioCommand::SetEffectMix: | ||
| if (cmd.effect_index >= 0 && | ||
| cmd.effect_index < static_cast<int>(effects_.size())) { | ||
| effects_[cmd.effect_index]->set_mix(cmd.value); | ||
| } | ||
| break; | ||
| case AudioCommand::SetInputGain: | ||
| input_gain_.store(cmd.value, std::memory_order_relaxed); | ||
| break; | ||
| case AudioCommand::SetOutputGain: | ||
| output_gain_.store(cmd.value, std::memory_order_relaxed); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
| // Existing engine logic commands draining sequence goes here... | ||
| } |
There was a problem hiding this comment.
Restore the real command draining logic here.
drain_commands() is a no-op now, so queued state changes are silently dropped instead of being applied on the audio thread. That breaks core control paths like parameter updates, enable/mix changes, and topology edits.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/audio/audio_engine_process.cpp` around lines 103 - 105, Restore real
draining in AudioEngine::drain_commands(): grab the command-queue under its
mutex, loop draining each queued command and apply it on the audio thread
instead of returning early — for each item call the appropriate apply/execute
handler (e.g. parameter-update handler, enable/mix-change handler, topology-edit
handler or the command object's apply/process method) and remove it from the
queue, then release the lock; ensure you use the existing queue and mutex member
names (e.g. queued_commands / command_queue and command_queue_mutex or their
actual equivalents) and clear any processed commands so queued state changes are
executed on the audio thread.
|
@Vinanthi07 Check the CI failures |
|
@Vinanthi07 Are you still working on this? Please check merge conflicts as well |
sudip-mondal-2002
left a comment
There was a problem hiding this comment.
@Vinanthi07 PTAL at CI failures
|
@Vinanthi07 Are you still working on this task? |
|
I am closing as I am not getting any responses, I'll reassign this, you may look for other items |
Summary of Changes
Implements a real-time, high-performance DSP Performance Profiler to track execution latency across individual pedal slots and global engine stability metrics.
Key Implementation Details:
src/audio/): Implemented lock-freestd::atomic<float>tracking utilizingstd::memory_order_relaxedinside the real-time audio thread to monitor timing blocks without processing jitter or overhead.src/gui/): Developed a layout panel using Dear ImGui to display sample rate benchmarks, buffer boundaries, safety fallback provisions, and reactive multi-color progress indicators tracking CPU processing capacity.Utilitiesmenu layout to toggle the profiler dashboard view overlay.closes [Feature] Real-Time Audio Latency Analyzer & DSP Performance Profiler #117
Summary by CodeRabbit