Skip to content

Performance: eliminate GC churn from getStateInfo in handleAudioChunk#230

Open
ysdede wants to merge 1 commit intomasterfrom
bolt/optimize-vad-state-allocation-13371080984200071641
Open

Performance: eliminate GC churn from getStateInfo in handleAudioChunk#230
ysdede wants to merge 1 commit intomasterfrom
bolt/optimize-vad-state-allocation-13371080984200071641

Conversation

@ysdede
Copy link
Copy Markdown
Owner

@ysdede ysdede commented Mar 24, 2026

What changed:

  • Exported a new VadState interface in src/lib/audio/AudioSegmentProcessor.ts.
  • Updated AudioSegmentProcessor.getStateInfo to accept an optional out?: VadState parameter, allowing zero-allocation mutations.
  • In src/lib/audio/AudioEngine.ts, cached a private processorStateInfo: VadState object.
  • Replaced the getStats() call inside AudioEngine.handleAudioChunk with the zero-allocation getStateInfo(this.processorStateInfo) call, reading noiseFloor and snr directly from the cached state.

Why it was needed:
In AudioEngine.handleAudioChunk, processing at roughly 100 chunks/second, the combination of getStats() and getStateInfo() was allocating at least 4 objects (CurrentStats, StatsSummary x 2, and the state info literal) per frame. This continuous allocation adds unnecessary GC pressure on the main thread during real-time audio capturing.

Impact:

  • Eliminates ~400 object allocations per second in the handleAudioChunk hot path.
  • Reduces high-frequency garbage collection churn during active microphone recording.
  • Completely preserves existing VAD metrics reporting and UI behavior.

How to verify:

  1. npm run test (All tests should pass, including AudioSegmentProcessor.test.ts).
  2. npm run build (Ensures the Vite project builds without TypeScript errors).
  3. Observe DevTools Memory Profiler during active recording: the allocation timeline should show drastically fewer Object allocations spawned by AudioEngine.ts.

PR created automatically by Jules for task 13371080984200071641 started by @ysdede

Summary by Sourcery

Reduce garbage-collection overhead in audio processing by introducing a reusable VAD state object and using zero-allocation state access in the audio engine hot path.

New Features:

  • Expose a reusable VadState interface for sharing voice activity detection state across audio components.

Enhancements:

  • Add an optional out parameter to AudioSegmentProcessor.getStateInfo to allow in-place updates without per-call allocations.
  • Cache a processorStateInfo instance in AudioEngine and use it to read VAD metrics instead of allocating new stats objects on each audio chunk.
  • Update project notes to document the zero-allocation state pattern used for high-frequency VAD callbacks.

Summary by CodeRabbit

Release Notes

  • Documentation

    • Added optimization documentation for high-frequency audio processing and voice activity detection workflows.
  • Refactor

    • Optimized memory management in the audio engine during real-time speech detection to improve performance and reduce garbage collection overhead.
    • Enhanced state management in audio processors to support efficient resource consumption in continuous analysis and high-frequency processing scenarios.

Replaced per-frame object allocation in AudioSegmentProcessor.getStateInfo
by introducing an optional `out: VadState` parameter.
AudioEngine now passes a cached processorStateInfo object into getStateInfo
and bypasses getStats entirely, eliminating ~400 object allocations
per second in the hot path.
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

The PR implements a zero-allocation state pattern for VAD callbacks by introducing a VadState interface, modifying AudioSegmentProcessor.getStateInfo() to accept an optional pre-allocated object for in-place mutation, and updating AudioEngine to cache and reuse this state object, eliminating per-chunk allocations.

Changes

Cohort / File(s) Summary
State Interface & Implementation
src/lib/audio/AudioSegmentProcessor.ts
Exported new VadState interface and modified getStateInfo() to accept optional out parameter for in-place mutation pattern, replacing inline object type.
Consumer Integration
src/lib/audio/AudioEngine.ts
Imported VadState, cached processorStateInfo instance, updated multiple calls to getStateInfo() to pass cached object, removed separate getStats() call.
Documentation
.jules/bolt.md
Added documentation describing the zero-allocation state pattern for high-frequency VAD callbacks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Performance: Optimize AudioSegmentProcessor allocations #184 — Both PRs introduce in-place mutation patterns in audio processing to reduce allocations; the related PR reuses currentStats with optional target parameter in summarizeSegmentStats, while this PR adds VadState type with optional out parameter to getStateInfo() and implements caller-side caching.

Poem

🐰 A rabbit hops through code so clean,
Reusing state, no leaks between,
Pre-allocated, mutated right,
Zero garbage, what a delight! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and precisely describes the main change: eliminating GC churn by optimizing getStateInfo usage, which is the core performance objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bolt/optimize-vad-state-allocation-13371080984200071641

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.

@qodo-code-review
Copy link
Copy Markdown

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Eliminate GC churn from VAD state allocation in handleAudioChunk

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Eliminate ~400 object allocations per second in handleAudioChunk hot path
• Export VadState interface for type-safe state mutations
• Add optional out parameter to getStateInfo for zero-allocation updates
• Cache processorStateInfo object in AudioEngine to reuse across frames
• Replace getStats() call with direct stateInfo access in metrics update
Diagram
flowchart LR
  A["AudioEngine.handleAudioChunk"] -->|calls| B["getStateInfo with cached object"]
  B -->|mutates in-place| C["processorStateInfo VadState"]
  C -->|zero allocations| D["Reduced GC pressure"]
  A -->|removed| E["getStats call"]
  E -->|eliminated| F["4 object allocations per frame"]
Loading

Grey Divider

File Changes

1. src/lib/audio/AudioEngine.ts Performance optimization +14/-7

Cache VadState and eliminate getStats calls

• Import new VadState interface from AudioSegmentProcessor
• Add private cached processorStateInfo object to prevent per-frame allocations
• Replace getStats() and getStateInfo() calls with single zero-allocation
 getStateInfo(this.processorStateInfo) call
• Update metrics extraction to read noiseFloor and snr directly from cached state object
• Pass cached state object to all getStateInfo() calls in isSpeechActive() and
 getSegmentsForVisualization()

src/lib/audio/AudioEngine.ts


2. src/lib/audio/AudioSegmentProcessor.ts ✨ Enhancement +16/-1

Add zero-allocation VadState interface and out parameter

• Export new VadState interface with inSpeech, noiseFloor, snr, and speechStartTime properties
• Add optional out parameter to getStateInfo method signature
• Implement in-place mutation logic when out parameter is provided
• Maintain backward compatibility by returning new object when out parameter is undefined

src/lib/audio/AudioSegmentProcessor.ts


3. .jules/bolt.md 📝 Documentation +3/-0

Document zero-allocation state pattern learning

• Document learning about zero-allocation state pattern for high-frequency callbacks
• Record solution of using optional out parameter for in-place object mutation

.jules/bolt.md


Grey Divider

Qodo Logo

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the performance of real-time audio processing by eliminating unnecessary object allocations in a critical hot path. Previously, the handleAudioChunk method generated approximately 400 temporary objects per second, leading to increased garbage collection pressure. By implementing a zero-allocation pattern using a pre-allocated state object, the changes drastically reduce GC churn, ensuring smoother and more efficient microphone recording without altering existing Voice Activity Detection (VAD) metrics or UI behavior.

Highlights

  • New Interface: Exported a new VadState interface in src/lib/audio/AudioSegmentProcessor.ts to define the structure of VAD state information.
  • Zero-Allocation getStateInfo: Updated AudioSegmentProcessor.getStateInfo to accept an optional out?: VadState parameter, enabling in-place mutation and zero-allocation updates.
  • Cached State Object: Introduced and cached a private processorStateInfo: VadState object within src/lib/audio/AudioEngine.ts to reuse for state updates.
  • Performance Optimization: Replaced the getStats() call and the original getStateInfo() call in AudioEngine.handleAudioChunk with the new zero-allocation getStateInfo(this.processorStateInfo) method, directly reading noiseFloor and snr from the cached state.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 24, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Remediation recommended

1. Falsy fallback masks metrics 🐞 Bug ✓ Correctness
Description
AudioEngine.handleAudioChunk now uses || to default noiseFloor/snr, which will replace any
falsy numeric (notably NaN, and potentially 0 if it ever occurs) instead of only defaulting on
null/undefined as before. This can silently hide bad upstream values and makes the metrics path
inconsistent with getSignalMetrics() which still uses ??.
Code

src/lib/audio/AudioEngine.ts[R614-615]

+        this.metrics.noiseFloor = stateInfo.noiseFloor || 0.01;
+        this.metrics.currentSNR = stateInfo.snr || 0;
Evidence
In the hot path, noiseFloor and snr are now assigned via logical-OR fallbacks. || treats NaN
as falsy and will coerce it to the default (masking the invalid value), whereas the previous code
used nullish coalescing (??) which would preserve NaN and only default on null/undefined.
Separately, AudioSegmentProcessor.updateStats() explicitly sets stats.snr to 0 when there are
no recent chunks, indicating that 0 is a meaningful value in the stats model, and
getSignalMetrics() continues to use ??, so this change also introduces inconsistency between
public metrics accessors.

src/lib/audio/AudioEngine.ts[596-620]
src/lib/audio/AudioEngine.ts[432-439]
src/lib/audio/AudioSegmentProcessor.ts[454-462]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`AudioEngine.handleAudioChunk` uses `||` for numeric defaults, which can mask `NaN` (and any other falsy numeric) and diverges from the previous semantics (`??`) and from `getSignalMetrics()`.

### Issue Context
- `getSignalMetrics()` still uses `??`.
- `AudioSegmentProcessor.updateStats()` models `snr` as `0` when there are no recent chunks.

### Fix Focus Areas
- src/lib/audio/AudioEngine.ts[608-616]

### Suggested fix
Prefer either:
- `this.metrics.noiseFloor = stateInfo.noiseFloor ?? 0.01;` and `this.metrics.currentSNR = stateInfo.snr ?? 0;`

or (more robust for numeric telemetry):
- guard with `Number.isFinite()` and (for noise floor) `> 0` to avoid silently converting invalid values into plausible defaults.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Non-type-only interface import 🐞 Bug ⚙ Maintainability
Description
VadState is imported as a normal named import even though it’s a TS-only interface, which reduces
clarity about runtime vs type dependencies. Using import type makes the intent explicit and avoids
accidental runtime coupling if the file later gains value exports with the same name or tooling
changes tighten type import rules.
Code

src/lib/audio/AudioEngine.ts[3]

+import { AudioSegmentProcessor, ProcessedSegment, VadState } from './AudioSegmentProcessor';
Evidence
VadState is declared as an interface (type-only) in AudioSegmentProcessor.ts, but
AudioEngine.ts imports it as a value. While modern TS/Vite typically elides type-only imports,
explicitly marking it as import type improves maintainability and makes the module’s runtime
surface area unambiguous.

src/lib/audio/AudioEngine.ts[1-4]
src/lib/audio/AudioSegmentProcessor.ts[50-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`VadState` is type-only but imported as a normal named import.

### Issue Context
This repo uses TypeScript with `isolatedModules`, and builds via Vite. Being explicit about type-only imports improves clarity and reduces accidental runtime import assumptions.

### Fix Focus Areas
- src/lib/audio/AudioEngine.ts[1-4]

### Suggested fix
Split the import:
```ts
import { AudioSegmentProcessor } from './AudioSegmentProcessor';
import type { ProcessedSegment, VadState } from './AudioSegmentProcessor';
```
(Optionally do the same for the interfaces imported from `./types`.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot bot commented Mar 24, 2026

Kilo Code Review could not run — your account is out of credits.

Add credits at app.kilo.ai to enable reviews on this change.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • Switching from ?? to || when setting noiseFloor and currentSNR changes the behavior for valid 0 values (e.g., noiseFloor = 0 now becomes 0.01); consider keeping nullish coalescing (??) to avoid treating 0 as falsy defaultable input.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Switching from `??` to `||` when setting `noiseFloor` and `currentSNR` changes the behavior for valid `0` values (e.g., `noiseFloor = 0` now becomes `0.01`); consider keeping nullish coalescing (`??`) to avoid treating `0` as falsy defaultable input.

## Individual Comments

### Comment 1
<location path="src/lib/audio/AudioEngine.ts" line_range="614-615" />
<code_context>
         this.metrics.peakEnergy = Math.max(this.metrics.peakEnergy * 0.99, energy);
-        this.metrics.noiseFloor = stats.noiseFloor ?? 0.01;
-        this.metrics.currentSNR = stats.snr ?? 0;
+        this.metrics.noiseFloor = stateInfo.noiseFloor || 0.01;
+        this.metrics.currentSNR = stateInfo.snr || 0;
         this.metrics.isSpeaking = stateInfo.inSpeech;

</code_context>
<issue_to_address>
**issue (bug_risk):** Using `||` instead of `??` changes behavior for valid zero values of noise floor and SNR.

Using `||` here will replace valid `0` values with the defaults, changing the semantics from the previous implementation. To preserve legitimate `0` noise floors and SNRs, keep nullish coalescing: `this.metrics.noiseFloor = stateInfo.noiseFloor ?? 0.01;` and `this.metrics.currentSNR = stateInfo.snr ?? 0;`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +614 to +615
this.metrics.noiseFloor = stateInfo.noiseFloor || 0.01;
this.metrics.currentSNR = stateInfo.snr || 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.

issue (bug_risk): Using || instead of ?? changes behavior for valid zero values of noise floor and SNR.

Using || here will replace valid 0 values with the defaults, changing the semantics from the previous implementation. To preserve legitimate 0 noise floors and SNRs, keep nullish coalescing: this.metrics.noiseFloor = stateInfo.noiseFloor ?? 0.01; and this.metrics.currentSNR = stateInfo.snr ?? 0;.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a zero-allocation state pattern for high-frequency Voice Activity Detection (VAD) callbacks within the audio engine. The AudioEngine now pre-allocates a VadState object and passes it as an optional out parameter to AudioSegmentProcessor.getStateInfo(). The getStateInfo() method in AudioSegmentProcessor has been updated to mutate this provided object in-place, eliminating new object allocations during frequent calls and reducing garbage collection churn. This optimization is applied to isSpeechActive(), handleAudioChunk(), and getAudioSegments() methods in AudioEngine. A new learning entry in .jules/bolt.md documents this strategy. There is no feedback to provide.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src/lib/audio/AudioEngine.ts`:
- Around line 614-615: Replace the use of logical OR fallbacks in the
AudioEngine assignment (this.metrics.noiseFloor and this.metrics.currentSNR)
with nullish coalescing to match getSignalMetrics(); specifically use ?? instead
of || and use the same fallback defaults as getSignalMetrics() (e.g., 0.0001 for
noiseFloor and 0 for SNR) so zero values are not treated as missing and the
defaults are consistent across getSignalMetrics() and the code that sets
this.metrics.noiseFloor and this.metrics.currentSNR.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c0ac2ed-5b8c-403a-a4a1-48a1cebad60c

📥 Commits

Reviewing files that changed from the base of the PR and between 474dbe6 and 20375bb.

📒 Files selected for processing (3)
  • .jules/bolt.md
  • src/lib/audio/AudioEngine.ts
  • src/lib/audio/AudioSegmentProcessor.ts

Comment on lines +614 to +615
this.metrics.noiseFloor = stateInfo.noiseFloor || 0.01;
this.metrics.currentSNR = stateInfo.snr || 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent fallback operators and values.

Using || here treats 0 as falsy and triggers the fallback, whereas getSignalMetrics() uses ?? (nullish coalescing). Additionally, the fallback values differ: 0.01 here vs 0.0001 in getSignalMetrics().

Consider using ?? for consistency and to avoid masking legitimate zero/low values:

🔧 Proposed fix
-        this.metrics.noiseFloor = stateInfo.noiseFloor || 0.01;
-        this.metrics.currentSNR = stateInfo.snr || 0;
+        this.metrics.noiseFloor = stateInfo.noiseFloor ?? 0.01;
+        this.metrics.currentSNR = stateInfo.snr ?? 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.metrics.noiseFloor = stateInfo.noiseFloor || 0.01;
this.metrics.currentSNR = stateInfo.snr || 0;
this.metrics.noiseFloor = stateInfo.noiseFloor ?? 0.01;
this.metrics.currentSNR = stateInfo.snr ?? 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/audio/AudioEngine.ts` around lines 614 - 615, Replace the use of
logical OR fallbacks in the AudioEngine assignment (this.metrics.noiseFloor and
this.metrics.currentSNR) with nullish coalescing to match getSignalMetrics();
specifically use ?? instead of || and use the same fallback defaults as
getSignalMetrics() (e.g., 0.0001 for noiseFloor and 0 for SNR) so zero values
are not treated as missing and the defaults are consistent across
getSignalMetrics() and the code that sets this.metrics.noiseFloor and
this.metrics.currentSNR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant