Skip to content

Performance: O(1) running sum in AudioEngine#215

Open
ysdede wants to merge 1 commit intomasterfrom
performance/audio-engine-running-sum-16629740528072009080
Open

Performance: O(1) running sum in AudioEngine#215
ysdede wants to merge 1 commit intomasterfrom
performance/audio-engine-running-sum-16629740528072009080

Conversation

@ysdede
Copy link
Owner

@ysdede ysdede commented Mar 19, 2026

What changed:
Refactored the reduce() operation in AudioEngine.handleAudioChunk to use an O(1) running sum (energyHistorySum) for the SMA energy calculation.

Why it was needed:
The handleAudioChunk method is a hot path processing high-frequency audio streams. Using .reduce() on every frame resulted in O(N) algorithmic complexity and introduced garbage collection churn from allocating callback functions.

Impact:
Eliminated high-frequency memory allocations and array iterations in the audio processing loop. Synthetic benchmark times showed macro performance matched original state, but fundamental CPU cycle count and GC pressure on the main thread is mathematically reduced.

How to verify:
Run the audio engine tests: bun test src/lib/audio/energy-calculation.test.ts
Run the audio engine visualization tests: bun test src/lib/audio/AudioEngine.visualization.test.ts


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

Summary by Sourcery

Optimize audio energy smoothing by replacing per-frame reductions with a constant-time running sum in the AudioEngine hot path.

Enhancements:

  • Use a running sum for SMA energy calculation in AudioEngine to eliminate per-chunk array reductions and reduce GC pressure.

Chores:

  • Update the engineering log with guidance on using running sums instead of array reductions in high-frequency processing loops.

Summary by CodeRabbit

  • Documentation

    • Added technical guidance for optimizing high-frequency data processing loops used in real-time audio systems.
  • Refactor

    • Enhanced audio engine's energy calculation for improved responsiveness and reduced computational overhead during chunk processing.
What changed: Refactored the `reduce()` operation in `AudioEngine.handleAudioChunk` to use an `O(1)` running sum (`energyHistorySum`) for the SMA energy calculation. Why it was needed: The `handleAudioChunk` method is a hot path processing high-frequency audio streams. Using `.reduce()` on every frame resulted in `O(N)` algorithmic complexity and introduced garbage collection churn from allocating callback functions. Impact: Eliminated high-frequency memory allocations and array iterations in the audio processing loop. Synthetic benchmark times showed macro performance matched original state, but fundamental CPU cycle count and GC pressure on the main thread is mathematically reduced. How to verify: Run the audio engine tests: `bun test src/lib/audio/energy-calculation.test.ts` Run the audio engine visualization tests: `bun test src/lib/audio/AudioEngine.visualization.test.ts`
@google-labs-jules
Copy link
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.

@qodo-code-review
Copy link

Review Summary by Qodo

Replace reduce with O(1) running sum in AudioEngine

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Replace O(N) reduce operation with O(1) running sum in audio processing • Eliminate per-frame array iterations and garbage collection overhead • Maintain energy history sum as persistent property for SMA calculation • Add performance learning documentation to Jules knowledge base 
Diagram
flowchart LR A["Audio chunk processing"] -->|push maxAbs| B["energyHistory array"] B -->|update sum| C["energyHistorySum property"] D["Remove old value"] -->|subtract from sum| C C -->|divide by length| E["SMA energy calculation"] F["Old: reduce operation"] -.->|O(N) complexity| G["Eliminated"] 
Loading

Grey Divider

File Changes

1. src/lib/audio/AudioEngine.ts ✨ Enhancement +4/-2

Implement O(1) running sum for energy calculation

• Added energyHistorySum property to track running sum of energy values • Updated energy history push to increment running sum in O(1) time • Modified history shift operation to decrement running sum when removing old values • Replaced .reduce() call with direct division of running sum by history length 

src/lib/audio/AudioEngine.ts


2. .jules/bolt.md 📝 Documentation +4/-0

Document running sum performance optimization pattern

• Added new learning entry about running sums in high-frequency loops • Documented performance impact of .reduce() in per-chunk processing • Captured action item to replace iteration methods with persistent running sums 

.jules/bolt.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 19, 2026

Code Review by Qodo

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

Grey Divider


Action required

1. Stale SMA after reset 🐞 Bug ✓ Correctness
Description
AudioEngine.reset() does not clear the SMA state (energyHistory/energyHistorySum), so the first ~6 chunks after a reset compute energy using peaks from the previous session. This can cause incorrect threshold/VAD decisions right after stop/start recording flows that call reset(). 
Code

src/lib/audio/AudioEngine.ts[R561-565]

+ this.energyHistorySum += maxAbs; if (this.energyHistory.length > 6) { - this.energyHistory.shift(); + this.energyHistorySum -= this.energyHistory.shift()!; } - const energy = this.energyHistory.reduce((a: number, b: number) => a + b, 0) / this.energyHistory.length; + const energy = this.energyHistorySum / this.energyHistory.length;
Evidence
handleAudioChunk computes SMA energy from persistent energyHistory/energyHistorySum state. reset() is explicitly intended to start a “new session” and is called when stopping recording / before restarting, but it does not clear that SMA state, so prior-session peaks remain in the average until the 6-sample window fully rotates. 

src/lib/audio/AudioEngine.ts[559-566]
src/lib/audio/AudioEngine.ts[355-392]
src/App.tsx[908-953]

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.reset()` is used to start a new recording/session, but it does not clear the SMA energy state (`energyHistory` and the newly added `energyHistorySum`). As a result, energy after reset is averaged with peaks from the previous session for up to the 6-sample window length. ### Issue Context - `handleAudioChunk()` maintains SMA state across chunks via `energyHistory`/`energyHistorySum`. - `reset()` claims to reset for a new session and is invoked in the app stop/start recording flow. ### Fix Focus Areas - src/lib/audio/AudioEngine.ts[359-392] - src/lib/audio/AudioEngine.ts[559-566] - src/App.tsx[908-953] ### Suggested change In `AudioEngine.reset()`, clear both: - `this.energyHistory = []` - `this.energyHistorySum = 0` (Place it alongside other session-state resets like `currentEnergy`/metrics.) 

ⓘ 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

@gemini-code-assist
Copy link

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 enhances the performance of the audio processing pipeline by optimizing the energy calculation within the AudioEngine. By transitioning from an O(N) array reduction to an O(1) running sum, the changes drastically reduce memory allocations and CPU overhead, ensuring smoother and more efficient handling of high-frequency audio streams.

Highlights

  • Performance Optimization: Refactored the AudioEngine.handleAudioChunk method to use an O(1) running sum (energyHistorySum) instead of reduce() for Simple Moving Average (SMA) energy calculation.
  • Reduced GC Churn: Eliminated high-frequency memory allocations and array iterations, which previously caused garbage collection overhead due to callback function allocations in the audio processing hot path.
  • Knowledge Capture: Documented the learning about using running sums in performance-critical loops in .jules/bolt.md for future reference and best practices.
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

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: test-and-build

Failed stage: Run tests [❌]

Failed test name: src/lib/audio/mel-e2e.test.ts > Real audio: life_Jim.wav

Failure summary:

The GitHub Action failed because npm test (Vitest) had 1 failing test suite:
src/lib/audio/mel-e2e.test.ts (Real audio: life_Jim.wav).
The failure is an HTTP 404 thrown during a
download step in src/lib/audio/mel-e2e.test.ts:297:67, where the test rejects if res.statusCode !==
200.
Additional log noise shows ECONNREFUSED while tenvad.worker tries to fetch()
http://localhost:3000/wasm/ten_vad.js, indicating no server was running on port 3000 in CI; however
those tests still reported as passing, and the run ultimately failed due to the HTTP 404 in the mel
E2E test.

Relevant error logs:
1: ##[group]Runner Image Provisioner 2: Hosted Compute Agent ... 132: ##[group]Run npm test 133: �[36;1mnpm test�[0m 134: shell: /usr/bin/bash -e {0} 135: ##[endgroup] 136: > keet@1.3.0 test 137: > vitest run 138: Using NPM parakeet.js (v1.4.0) 139: �[1m�[46m RUN �[49m�[22m �[36mv4.1.0 �[39m�[90m/home/runner/work/keet/keet�[39m 140: �[90mstdout�[2m | src/lib/audio/mel-e2e.test.ts�[2m > �[22m�[2mCross-validation against ONNX reference�[2m > �[22m�[2mshould load mel_reference.json from parakeet.js 141: �[22m�[39mSKIP: mel_reference.json not found at /home/runner/work/keet/parakeet.js/tests/mel_reference.json 142: Run: cd ../parakeet.js && python tests/generate_mel_reference.py 143: �[90mstdout�[2m | src/lib/audio/mel-e2e.test.ts�[2m > �[22m�[2mCross-validation against ONNX reference�[2m > �[22m�[2mshould match ONNX mel filterbank within 1e-5 144: �[22m�[39mSKIP: No filterbank reference 145: �[90mstdout�[2m | src/lib/audio/mel-e2e.test.ts�[2m > �[22m�[2mCross-validation against ONNX reference�[2m > �[22m�[2mshould match ONNX full pipeline for each test signal (max<0.05, mean<0.005) 146: �[22m�[39mSKIP: No reference data 147: �[90mstdout�[2m | src/lib/buffer/buffer.worker.test.ts�[2m > �[22m�[2mbuffer.worker�[2m > �[22m�[2mshould load without errors 148: �[22m�[39m[BufferWorker] Initialized: sr=16000, audio=1.8MB, mel=1.5MB, energyVad hop=1280, inferenceVad hop=256 ... 485: noiseFloor: �[32m'0.005000'�[39m 486: } 487: [AudioSegmentProcessor] Speech start detected { 488: detectedAt: �[32m'0.10'�[39m, 489: actualStart: �[32m'0.10'�[39m, 490: lookbackDiff: �[32m'0.00'�[39m, 491: snr: �[32m'16.02'�[39m, 492: noiseFloor: �[32m'0.005000'�[39m 493: } 494: �[32m✓�[39m src/lib/audio/AudioEngine.visualization.test.ts �[2m(�[22m�[2m8 tests�[22m�[2m)�[22m�[32m 127�[2mms�[22m�[39m 495: �[32m✓�[39m src/lib/audio/mel.worker.test.ts �[2m(�[22m�[2m9 tests�[22m�[2m)�[22m�[33m 1146�[2mms�[22m�[39m 496: �[33m�[2m✓�[22m�[39m should return normalized features for pushed audio �[33m 527�[2mms�[22m�[39m 497: �[32m✓�[39m src/lib/audio/RingBuffer.test.ts �[2m(�[22m�[2m16 tests�[22m�[2m)�[22m�[32m 118�[2mms�[22m�[39m 498: �[32m✓�[39m src/lib/transcription/UtteranceBasedMerger.test.ts �[2m(�[22m�[2m12 tests�[22m�[2m)�[22m�[32m 16�[2mms�[22m�[39m 499: �[32m✓�[39m src/lib/transcription/preprocessor-selection.test.ts �[2m(�[22m�[2m12 tests�[22m�[2m)�[22m�[32m 10�[2mms�[22m�[39m 500: AggregateError:  501: at internalConnectMultiple (node:net:1122:18) 502: at afterConnectMultiple (node:net:1689:7) { 503: code: 'ECONNREFUSED', 504: [errors]: [ 505: Error: connect ECONNREFUSED ::1:3000 506: at createConnectionError (node:net:1652:14) 507: at afterConnectMultiple (node:net:1682:16) { 508: errno: -111, 509: code: 'ECONNREFUSED', 510: syscall: 'connect', 511: address: '::1', 512: port: 3000 513: }, 514: Error: connect ECONNREFUSED 127.0.0.1:3000 515: at createConnectionError (node:net:1652:14) 516: at afterConnectMultiple (node:net:1682:16) { 517: errno: -111, 518: code: 'ECONNREFUSED', 519: syscall: 'connect', 520: address: '127.0.0.1', 521: port: 3000 522: } 523: ] 524: } 525: �[90mstderr�[2m | src/lib/vad/tenvad.worker.test.ts�[2m > �[22m�[2mtenvad.worker�[2m > �[22m�[2mshould load without errors 526: �[22m�[39m[TenVAD Worker] Init failed: DOMException [NetworkError]: Failed to execute "fetch()" on "Window" with URL "http://localhost:3000/wasm/ten_vad.js":  527: at Fetch.onError �[90m(file:///home/runner/work/keet/keet/�[39mnode_modules/�[4mhappy-dom�[24m/lib/fetch/Fetch.js:540:21�[90m)�[39m 528: �[90m at ClientRequest.emit (node:events:524:28)�[39m 529: �[90m at emitErrorEvent (node:_http_client:101:11)�[39m 530: �[90m at Socket.socketErrorListener (node:_http_client:504:5)�[39m 531: �[90m at Socket.emit (node:events:524:28)�[39m 532: �[90m at emitErrorNT (node:internal/streams/destroy:169:8)�[39m 533: �[90m at emitErrorCloseNT (node:internal/streams/destroy:128:3)�[39m 534: �[90m at processTicksAndRejections (node:internal/process/task_queues:82:21)�[39m 535: AggregateError:  536: at internalConnectMultiple (node:net:1122:18) 537: at afterConnectMultiple (node:net:1689:7) { 538: code: 'ECONNREFUSED', 539: [errors]: [ 540: Error: connect ECONNREFUSED ::1:3000 541: at createConnectionError (node:net:1652:14) 542: at afterConnectMultiple (node:net:1682:16) { 543: errno: -111, 544: code: 'ECONNREFUSED', 545: syscall: 'connect', 546: address: '::1', 547: port: 3000 548: }, 549: Error: connect ECONNREFUSED 127.0.0.1:3000 550: at createConnectionError (node:net:1652:14) 551: at afterConnectMultiple (node:net:1682:16) { 552: errno: -111, 553: code: 'ECONNREFUSED', 554: syscall: 'connect', 555: address: '127.0.0.1', 556: port: 3000 557: } 558: ] 559: } 560: �[90mstderr�[2m | src/lib/vad/tenvad.worker.test.ts�[2m > �[22m�[2mtenvad.worker�[2m > �[22m�[2mshould respond to INIT with either success or ERROR 561: �[22m�[39m[TenVAD Worker] Init failed: DOMException [NetworkError]: Failed to execute "fetch()" on "Window" with URL "http://localhost:3000/wasm/ten_vad.js":  562: at Fetch.onError �[90m(file:///home/runner/work/keet/keet/�[39mnode_modules/�[4mhappy-dom�[24m/lib/fetch/Fetch.js:540:21�[90m)�[39m 563: �[90m at ClientRequest.emit (node:events:524:28)�[39m 564: �[90m at emitErrorEvent (node:_http_client:101:11)�[39m 565: �[90m at Socket.socketErrorListener (node:_http_client:504:5)�[39m 566: �[90m at Socket.emit (node:events:524:28)�[39m 567: �[90m at emitErrorNT (node:internal/streams/destroy:169:8)�[39m 568: �[90m at emitErrorCloseNT (node:internal/streams/destroy:128:3)�[39m 569: �[90m at processTicksAndRejections (node:internal/process/task_queues:82:21)�[39m 570: AggregateError:  571: at internalConnectMultiple (node:net:1122:18) 572: at afterConnectMultiple (node:net:1689:7) { 573: code: 'ECONNREFUSED', 574: [errors]: [ 575: Error: connect ECONNREFUSED ::1:3000 576: at createConnectionError (node:net:1652:14) 577: at afterConnectMultiple (node:net:1682:16) { 578: errno: -111, 579: code: 'ECONNREFUSED', 580: syscall: 'connect', 581: address: '::1', 582: port: 3000 583: }, 584: Error: connect ECONNREFUSED 127.0.0.1:3000 585: at createConnectionError (node:net:1652:14) 586: at afterConnectMultiple (node:net:1682:16) { 587: errno: -111, 588: code: 'ECONNREFUSED', 589: syscall: 'connect', 590: address: '127.0.0.1', 591: port: 3000 592: } 593: ] 594: } 595: �[90mstderr�[2m | src/lib/vad/tenvad.worker.test.ts�[2m > �[22m�[2mtenvad.worker�[2m > �[22m�[2mwhen INIT succeeds�[2m > �[22m�[2mshould respond to RESET with success 596: �[22m�[39m[TenVAD Worker] Init failed: DOMException [NetworkError]: Failed to execute "fetch()" on "Window" with URL "http://localhost:3000/wasm/ten_vad.js":  597: at Fetch.onError �[90m(file:///home/runner/work/keet/keet/�[39mnode_modules/�[4mhappy-dom�[24m/lib/fetch/Fetch.js:540:21�[90m)�[39m 598: �[90m at ClientRequest.emit (node:events:524:28)�[39m 599: �[90m at emitErrorEvent (node:_http_client:101:11)�[39m 600: �[90m at Socket.socketErrorListener (node:_http_client:504:5)�[39m 601: �[90m at Socket.emit (node:events:524:28)�[39m 602: �[90m at emitErrorNT (node:internal/streams/destroy:169:8)�[39m 603: �[90m at emitErrorCloseNT (node:internal/streams/destroy:128:3)�[39m 604: �[90m at processTicksAndRejections (node:internal/process/task_queues:82:21)�[39m 605: AggregateError:  606: at internalConnectMultiple (node:net:1122:18) 607: at afterConnectMultiple (node:net:1689:7) { 608: code: 'ECONNREFUSED', 609: [errors]: [ 610: Error: connect ECONNREFUSED ::1:3000 611: at createConnectionError (node:net:1652:14) 612: at afterConnectMultiple (node:net:1682:16) { 613: errno: -111, 614: code: 'ECONNREFUSED', 615: syscall: 'connect', 616: address: '::1', 617: port: 3000 618: }, 619: Error: connect ECONNREFUSED 127.0.0.1:3000 620: at createConnectionError (node:net:1652:14) 621: at afterConnectMultiple (node:net:1682:16) { 622: errno: -111, 623: code: 'ECONNREFUSED', 624: syscall: 'connect', 625: address: '127.0.0.1', 626: port: 3000 627: } 628: ] 629: } 630: �[90mstderr�[2m | src/lib/vad/tenvad.worker.test.ts�[2m > �[22m�[2mtenvad.worker�[2m > �[22m�[2mwhen INIT succeeds�[2m > �[22m�[2mshould return RESULT with valid shape when PROCESS receives one full hop of silence 631: �[22m�[39m[TenVAD Worker] Init failed: DOMException [NetworkError]: Failed to execute "fetch()" on "Window" with URL "http://localhost:3000/wasm/ten_vad.js":  632: at Fetch.onError �[90m(file:///home/runner/work/keet/keet/�[39mnode_modules/�[4mhappy-dom�[24m/lib/fetch/Fetch.js:540:21�[90m)�[39m 633: �[90m at ClientRequest.emit (node:events:524:28)�[39m 634: �[90m at emitErrorEvent (node:_http_client:101:11)�[39m 635: �[90m at Socket.socketErrorListener (node:_http_client:504:5)�[39m 636: �[90m at Socket.emit (node:events:524:28)�[39m 637: �[90m at emitErrorNT (node:internal/streams/destroy:169:8)�[39m 638: �[90m at emitErrorCloseNT (node:internal/streams/destroy:128:3)�[39m 639: �[90m at processTicksAndRejections (node:internal/process/task_queues:82:21)�[39m 640: �[32m✓�[39m src/lib/vad/tenvad.worker.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 75�[2mms�[22m�[39m 641: �[32m✓�[39m src/lib/vad/EnergyVAD.test.ts �[2m(�[22m�[2m7 tests�[22m�[2m)�[22m�[32m 26�[2mms�[22m�[39m 642: �[32m✓�[39m src/lib/transcription/UtteranceBasedMerger.regression.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 14�[2mms�[22m�[39m 643: �[32m✓�[39m src/lib/vad/VADRingBuffer.test.ts �[2m(�[22m�[2m15 tests�[22m�[2m)�[22m�[32m 13�[2mms�[22m�[39m 644: �[32m✓�[39m src/lib/transcription/TranscriptionWorkerClient.test.ts �[2m(�[22m�[2m6 tests�[22m�[2m)�[22m�[32m 20�[2mms�[22m�[39m 645: �[32m✓�[39m src/utils/settingsStorage.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 11�[2mms�[22m�[39m 646: �[32m✓�[39m src/lib/vad/TenVADWorkerClient.test.ts �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[32m 10�[2mms�[22m�[39m 647: �[32m✓�[39m src/lib/transcription/TokenStreamTranscriber.test.ts �[2m(�[22m�[2m2 tests�[22m�[2m)�[22m�[32m 117�[2mms�[22m�[39m 648: �[32m✓�[39m src/lib/audio/energy-calculation.test.ts �[2m(�[22m�[2m4 tests�[22m�[2m)�[22m�[32m 6�[2mms�[22m�[39m 649: �[32m✓�[39m src/csp.test.ts �[2m(�[22m�[2m1 test�[22m�[2m)�[22m�[32m 18�[2mms�[22m�[39m 650: �[32m✓�[39m src/utils/time.test.ts �[2m(�[22m�[2m5 tests�[22m�[2m)�[22m�[32m 5�[2mms�[22m�[39m 651: �[31m⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Failed Suites 1 �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m 652: �[41m�[1m FAIL �[22m�[49m src/lib/audio/mel-e2e.test.ts�[2m > �[22mReal audio: life_Jim.wav 653: �[31m�[1mError�[22m: HTTP 404�[39m 654: �[36m �[2m❯�[22m ClientRequest.<anonymous> src/lib/audio/mel-e2e.test.ts:�[2m297:67�[22m�[39m 655: �[90m295|�[39m return download(res.headers.location, redi… 656: �[90m296|�[39m } 657: �[90m297|�[39m if (res.statusCode !== 200) return reject(new … 658: �[90m |�[39m �[31m^�[39m 659: �[90m298|�[39m �[35mconst�[39m chunks�[33m:�[39m �[33mBuffer�[39m[] �[33m=�[39m []�[33m;�[39m 660: �[90m299|�[39m res.on('data', (chunk: Buffer) => chunks.push(… 661: �[31m�[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯�[22m�[39m 662: �[2m Test Files �[22m �[1m�[31m1 failed�[39m�[22m�[2m | �[22m�[1m�[32m21 passed�[39m�[22m�[90m (22)�[39m 663: �[2m Tests �[22m �[1m�[32m207 passed�[39m�[22m�[2m | �[22m�[33m8 skipped�[39m�[90m (215)�[39m 664: �[2m Start at �[22m 16:10:33 665: �[2m Duration �[22m 6.23s�[2m (transform 1.11s, setup 0ms, import 1.69s, tests 2.96s, environment 9.29s)�[22m 666: ##[error]Error: HTTP 404 667: ❯ ClientRequest.<anonymous> src/lib/audio/mel-e2e.test.ts:297:67 668: 669: 670: ##[error]Process completed with exit code 1. 671: Post job cleanup. 
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

A performance optimization was implemented in AudioEngine's energy smoothing calculation, replacing iterative reduce-based rolling sum computation with an incremental running-sum accumulator pattern. This change is documented as guidance in the project's performance log.

Changes

Cohort / File(s) Summary
Documentation
.jules/bolt.md
Added dated entry (2025-05-18) documenting O(1) running-sum optimization strategy for moving average calculations to reduce GC churn and CPU overhead.
Audio Engine Refactor
src/lib/audio/AudioEngine.ts
Replaced energyHistory.reduce() pattern with incremental energyHistorySum accumulator; adds maxAbs on each chunk and subtracts evicted values when history exceeds 6 entries.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 With whiskers twitching at the speed,
We sum in O(1), what joy indeed!
No reduce loops slowing down the beat,
Just running totals, neat and fleet.

🚥 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 title directly reflects the main change: refactoring AudioEngine to use O(1) running sum logic instead of reduce()-based iterations for performance optimization.
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 performance/audio-engine-running-sum-16629740528072009080
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
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 left some high level feedback:

  • Consider removing the non-null assertion on this.energyHistory.shift()! by guarding on this.energyHistory.length or using a local variable so the code is safer against future changes to the history management logic.
  • It may be worth extracting the SMA window size 6 into a named constant or class property to avoid magic numbers and make future tuning of the window length easier.
  • Since energyHistorySum is now a stateful accumulator, double-check that any existing reset/teardown paths for the engine (e.g., when starting/stopping streams) also reset this field to keep energy calculations consistent across sessions.
Prompt for AI Agents
Please address the comments from this code review: ## Overall Comments - Consider removing the non-null assertion on `this.energyHistory.shift()!` by guarding on `this.energyHistory.length` or using a local variable so the code is safer against future changes to the history management logic. - It may be worth extracting the SMA window size `6` into a named constant or class property to avoid magic numbers and make future tuning of the window length easier. - Since `energyHistorySum` is now a stateful accumulator, double-check that any existing reset/teardown paths for the engine (e.g., when starting/stopping streams) also reset this field to keep energy calculations consistent across sessions.

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.
Copy link

@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 introduces a performance optimization in AudioEngine by replacing a .reduce() call with a running sum for calculating the simple moving average of audio energy. This is a great improvement for a hot path, reducing algorithmic complexity to O(1) and minimizing garbage collection. The implementation is correct, but I've found one issue: the new state variable energyHistorySum (and the existing energyHistory) is not cleared in the reset() method, which could lead to incorrect calculations after a reset. I've also left a minor comment on the engineering log file regarding entry ordering.


// SMA buffer for energy calculation
private energyHistory: number[] = [];
private energyHistorySum: number = 0;

Choose a reason for hiding this comment

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

high

The new energyHistorySum property is not cleared in the reset() method. This will cause it to persist across resets, leading to incorrect energy calculations. The related energyHistory array also isn't being reset.

To fix this, please add the following to the reset() method:

this.energyHistory = []; this.energyHistorySum = 0;
Comment on lines +1 to +3
## 2025-05-18 - Running sums in high-frequency loops
Learning: In performance-critical per-chunk processing loops like audio processing, using `.reduce()` for moving averages causes GC overhead and O(N) complexity per frame.
Action: Replace `.reduce()` and related iteration methods with O(1) running sums using persistent properties (e.g. `energySum`) to minimize GC churn and CPU overhead per tick.

Choose a reason for hiding this comment

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

medium

To maintain consistency, it's helpful to keep this log file sorted. The new entry appears to be out of order relative to the existing entry. Please ensure entries are sorted, for example, in reverse chronological order.

this.energyHistorySum += maxAbs;
if (this.energyHistory.length > 6) {
this.energyHistory.shift();
this.energyHistorySum -= this.energyHistory.shift()!;
Copy link

Choose a reason for hiding this comment

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

SUGGESTION: Consider using optional chaining or explicit undefined handling instead of non-null assertion (!). While the length check (> 6) makes this safe, explicit handling improves readability and defensive coding.

Suggested change
this.energyHistorySum -= this.energyHistory.shift()!;
this.energyHistorySum -= this.energyHistory.shift() ?? 0;
@kilo-code-bot
Copy link

kilo-code-bot bot commented Mar 19, 2026

Code Review Summary

Status: 1 Suggestion Found | Recommendation: Merge with optional fix

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
src/lib/audio/AudioEngine.ts 563 Non-null assertion could be replaced with explicit undefined handling for defensive coding

Performance Review

No performance concerns. The change is a valid O(N) → O(1) optimization:

  • Replaced .reduce() (O(N) per frame) with a running sum (O(1))
  • Eliminates per-frame array iteration and GC callback allocations
  • Maintains constant memory with fixed-size 6-element buffer
  • This is exactly the pattern documented in .jules/bolt.md

Security Review

No security issues identified. This is a pure computation optimization with no security implications.

Reliability Review

No reliability concerns identified. The algorithm is mathematically equivalent to the original implementation:

  • Running sum correctly maintained on push (add) and shift (subtract)
  • Division by array length produces identical SMA results
  • Length check > 6 ensures shift only occurs when array has 7+ elements

Test Review

Test coverage is adequate. The existing tests in energy-calculation.test.ts validate the algorithm correctness. Note: Tests use the reference implementation (reduce) but verify the same mathematical properties - the optimization doesn't change correctness, just efficiency.

Other Observations

⚠️ The reset() method does not clear energyHistory or energyHistorySum. This is a pre-existing issue in the codebase (not introduced by this PR), as the original code also didn't reset energyHistory. This was already noted by another reviewer.

Files Reviewed (2 files)
  • src/lib/audio/AudioEngine.ts - 1 suggestion
  • .jules/bolt.md - No issues

Merge Risk: LOW - The change is a well-targeted performance optimization with no correctness or security implications.

Comment on lines +561 to +565
this.energyHistorySum += maxAbs;
if (this.energyHistory.length > 6) {
this.energyHistory.shift();
this.energyHistorySum -= this.energyHistory.shift()!;
}
const energy = this.energyHistory.reduce((a: number, b: number) => a + b, 0) / this.energyHistory.length;
const energy = this.energyHistorySum / this.energyHistory.length;

Choose a reason for hiding this comment

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

Action required

1. Stale sma after reset 🐞 Bug ✓ Correctness

AudioEngine.reset() does not clear the SMA state (energyHistory/energyHistorySum), so the first ~6 chunks after a reset compute energy using peaks from the previous session. This can cause incorrect threshold/VAD decisions right after stop/start recording flows that call reset(). 
Agent Prompt
### Issue description `AudioEngine.reset()` is used to start a new recording/session, but it does not clear the SMA energy state (`energyHistory` and the newly added `energyHistorySum`). As a result, energy after reset is averaged with peaks from the previous session for up to the 6-sample window length. ### Issue Context - `handleAudioChunk()` maintains SMA state across chunks via `energyHistory`/`energyHistorySum`. - `reset()` claims to reset for a new session and is invoked in the app stop/start recording flow. ### Fix Focus Areas - src/lib/audio/AudioEngine.ts[359-392] - src/lib/audio/AudioEngine.ts[559-566] - src/App.tsx[908-953] ### Suggested change In `AudioEngine.reset()`, clear both: - `this.energyHistory = []` - `this.energyHistorySum = 0` (Place it alongside other session-state resets like `currentEnergy`/metrics.) 

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

Copy link

@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`: - Line 55: reset() currently doesn't clear the running-sum state so new sessions inherit stale SMA values; update the reset implementation to also clear energyHistorySum and energyHistory (the accumulator and its array) so both are reinitialized when reset() is called — locate the reset() method in AudioEngine and set energyHistorySum = 0 and energyHistory = [] (or equivalent reinitialization) alongside the other reset logic. 

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0da74b0a-c20f-4dae-8f55-09ec2f77e9db

📥 Commits

Reviewing files that changed from the base of the PR and between be505c3 and 69918cd.

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

// SMA buffer for energy calculation
private energyHistory: number[] = [];
private energyHistorySum: number = 0;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset path should clear the new running-sum state.

Line 55 adds persistent accumulator state, but reset() does not clear energyHistory/energyHistorySum. A new session can start with stale SMA state from the prior session.

🔧 Proposed fix
diff --git a/src/lib/audio/AudioEngine.ts b/src/lib/audio/AudioEngine.ts @@ reset(): void { // Reset audio/VAD state this.ringBuffer.reset(); this.audioProcessor.reset(); this.currentEnergy = 0; + this.energyHistory = []; + this.energyHistorySum = 0; @@ // Clear segment history used by the visualizer this.recentSegments = []; this.energyBarHistory = [];
📝 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
private energyHistorySum: number = 0;
reset(): void {
// Reset audio/VAD state
this.ringBuffer.reset();
this.audioProcessor.reset();
this.currentEnergy = 0;
this.energyHistory = [];
this.energyHistorySum = 0;
// Clear segment history used by the visualizer
this.recentSegments = [];
this.energyBarHistory = [];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/audio/AudioEngine.ts` at line 55, reset() currently doesn't clear the running-sum state so new sessions inherit stale SMA values; update the reset implementation to also clear energyHistorySum and energyHistory (the accumulator and its array) so both are reinitialized when reset() is called — locate the reset() method in AudioEngine and set energyHistorySum = 0 and energyHistory = [] (or equivalent reinitialization) alongside the other reset logic. 
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant