Skip to content

feat: implement real-time DSP latency profiler and GUI dashboard (#117)#121

Closed
Vinanthi07 wants to merge 3 commits into
sudip-mondal-2002:mainfrom
Vinanthi07:add-DSPProfiler-class-for-global-latency-tracking
Closed

feat: implement real-time DSP latency profiler and GUI dashboard (#117)#121
Vinanthi07 wants to merge 3 commits into
sudip-mondal-2002:mainfrom
Vinanthi07:add-DSPProfiler-class-for-global-latency-tracking

Conversation

@Vinanthi07

@Vinanthi07 Vinanthi07 commented May 18, 2026

Copy link
Copy Markdown

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:

  • Core Engine Data Collection (src/audio/): Implemented lock-free std::atomic<float> tracking utilizing std::memory_order_relaxed inside the real-time audio thread to monitor timing blocks without processing jitter or overhead.
  • GUI Dashboard Framework (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.
  • Menu Bar Integration: Exposed a navigation hook under the Utilities menu layout to toggle the profiler dashboard view overlay.
  • Static Analysis Compliance: Structured color thresholds using descriptive constants to adhere to clean coding conventions.
    closes [Feature] Real-Time Audio Latency Analyzer & DSP Performance Profiler #117

Summary by CodeRabbit

  • New Features
    • Added DSP Performance Profiler UI to monitor audio processing performance.
    • Displays CPU load, total callback time, and per-effect processing times with progress-bar visuals and high-load warnings.
    • Added "DSP Performance Profiler" menu option under Utilities to show/hide the profiler.

Review Change Stack

…tants

LiveReview Pre-Commit Check: ran (iter:1, coverage:0%)
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The 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.

Changes

DSP Performance Profiler Feature

Layer / File(s) Summary
Per-effect DSP timing capture
src/audio/audio_engine.h, src/audio/audio_engine_process.cpp
AudioEngine adds MAX_PROFILED_EFFECTS and atomic effect_process_times_us_, exposes get_total_callback_us() and get_effect_process_time_us(int), and records per-effect durations in process_audio.
Profiler UI and state
src/gui/gui_manager.h, src/gui/gui_manager_frame.cpp
GuiManager adds render_profiler() and show_profiler_, conditionally renders a "DSP Performance Profiler" ImGui window showing sample rate/buffer, total callback time, cpu_load, and per-pedal processing bars.
Menu access to profiler
src/gui/gui_manager_menu.cpp
Adds "DSP Performance Profiler" under Utilities that toggles show_profiler_.

Estimated code review effort:
🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs:

Suggested labels:
type:feature, level:advanced

Suggested reviewers:

  • sudip-mondal-2002

"I hopped through signals, timing each stomp,
Measuring pedals as they make the sound plop.
Atomics hold moments in microsecond light,
Bars glow and warn when the CPU's tight.
A rabbit celebrates the profiling crop."

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR implements core profiling features (#117): per-effect DSP timing tracking via atomic variables, GUI dashboard showing CPU load and per-pedal processing times with visual indicators, menu integration, and real-time low-overhead monitoring. However, several objectives are not addressed: latency graphs/history, buffer underrun/overrun detection, warning thresholds for high latency, and floating overlay UI option. Clarify whether unimplemented features (performance graphs, buffer monitoring, threshold warnings) are intentionally deferred or should be included; current implementation covers core profiling infrastructure but not all proposed monitoring capabilities.
Out of Scope Changes check ❓ Inconclusive Changes focus on profiling infrastructure (timing collection, GUI rendering, menu integration) and drain_commands simplification, all directly supporting the profiler feature. The drain_commands replacement appears structural rather than feature-scope but warrants clarification. Verify that replacing drain_commands with a stub is intentional and does not inadvertently remove required effect parameter/gain command handling outside this PR's scope.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement real-time DSP latency profiler and GUI dashboard' accurately describes the main change: adding a profiler and GUI dashboard for DSP performance monitoring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7e333c9 and 88f07cc.

📒 Files selected for processing (5)
  • src/audio/audio_engine.h
  • src/audio/audio_engine_process.cpp
  • src/gui/gui_manager.h
  • src/gui/gui_manager_frame.cpp
  • src/gui/gui_manager_menu.cpp

Comment thread src/gui/gui_manager_frame.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 88f07cc and f55eb88.

📒 Files selected for processing (2)
  • src/audio/audio_engine_process.cpp
  • src/gui/gui_manager_frame.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/gui/gui_manager_frame.cpp

Comment on lines +29 to 32
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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.

Comment on lines +71 to +76
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines 103 to 105
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...
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

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.

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

@Vinanthi07 Check the CI failures

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

@Vinanthi07 Are you still working on this? Please check merge conflicts as well

@sudip-mondal-2002 sudip-mondal-2002 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Vinanthi07 PTAL at CI failures

@sudip-mondal-2002 sudip-mondal-2002 added type:feature New feature or request High Impact Significant user-facing improvement type:performance Performance optimization or latency improvement level:critical type:devops labels May 24, 2026
@sudip-mondal-2002

Copy link
Copy Markdown
Owner

@Vinanthi07 Are you still working on this task?

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

I am closing as I am not getting any responses, I'll reassign this, you may look for other items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

High Impact Significant user-facing improvement level:critical type:devops type:feature New feature or request type:performance Performance optimization or latency improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Real-Time Audio Latency Analyzer & DSP Performance Profiler

2 participants