Skip to content

Fix: Multiple memory leaks and resource management issues#4209

Merged
katspaugh merged 6 commits intomainfrom
fix/memory-leaks-combined
Oct 1, 2025
Merged

Fix: Multiple memory leaks and resource management issues#4209
katspaugh merged 6 commits intomainfrom
fix/memory-leaks-combined

Conversation

@katspaugh
Copy link
Owner

Summary

This PR addresses 6 critical memory leaks and resource management issues identified through
code analysis. These fixes prevent memory accumulation during long-running sessions and
improve overall stability.

Issues Fixed

1. Timer infinite loop memory leak (src/timer.ts)

Severity: CRITICAL

  • Problem: Event-based tick loop continuously added new listeners
  • Fix: Replaced with direct requestAnimationFrame loop with proper cancellation
  • Impact: Prevents memory growth and CPU waste during long playback sessions

2. EventEmitter once() memory leak (src/event-emitter.ts)

Severity: HIGH

  • Problem: Recursive subscription created duplicate listeners
  • Fix: Single wrapper function that removes itself after execution
  • Impact: Prevents listener accumulation in event system

3. WebAudioPlayer bufferNode leak (src/webaudio.ts)

Severity: HIGH

  • Problem: Old bufferNode event handlers not removed before disconnect
  • Fix: Explicitly set onended to null before disconnecting
  • Impact: Prevents memory accumulation during rapid play/pause cycles

4. Regions plugin subscription leak (src/plugins/regions.ts)

Severity: MEDIUM

  • Problem: Double-subscription tracking and improper cleanup
  • Fix: Separated cleanup logic, removed duplicate tracking
  • Impact: Prevents orphaned event listeners on wavesurfer instances

5. AbortController race condition (src/wavesurfer.ts)

Severity: MEDIUM

  • Problem: Concurrent loads could interfere, unnecessary optional chaining
  • Fix: Abort previous fetch before starting new one
  • Impact: Ensures only one fetch is active, prevents race conditions

6. Renderer drag handler accumulation (src/renderer.ts)

Severity: MEDIUM

  • Problem: setOptions() re-initialized drag without checking if already setup
  • Fix: Track initialization state, return early if already initialized
  • Impact: Prevents duplicate drag handlers and memory leaks

Testing Recommendations

  1. Long-running sessions: Test playback over extended periods
  2. Rapid interactions: Test frequent play/pause/seek operations
  3. Region stress test: Create/delete many regions repeatedly
  4. Multiple loads: Call load() multiple times in quick succession
  5. Memory profiling: Use Chrome DevTools to verify no memory growth

Related Issues

  • Improves overall memory management and stability
  • No breaking changes to public API

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

katspaugh and others added 6 commits October 1, 2025 14:36
The Timer class had a critical memory leak caused by an infinite
requestAnimationFrame loop that continuously added new event listeners.

Issues fixed:
- Removed event-based tick loop that accumulated listeners in memory
- Added proper animationFrameId tracking for cancellation
- Added isRunning flag to prevent multiple simultaneous loops
- Implemented proper cleanup with cancelAnimationFrame()

The timer now uses a direct requestAnimationFrame pattern without
event subscriptions, properly manages resources, and can be cleanly
stopped without leaking memory or consuming CPU cycles.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The once() implementation had a memory leak caused by adding two
listeners (the original and a wrapper) through a recursive on() call.

Issues fixed:
- Removed recursive this.on() call that created duplicate listeners
- Now uses a single wrapper function that removes itself after execution
- Wrapper properly cleans up by removing itself before calling original listener
- Unsubscribe function now correctly references the wrapper

The once() pattern now properly adds only one listener that removes
itself after the first event emission, preventing memory leaks from
accumulated listener references.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The _play() method had a memory leak caused by not properly cleaning up
old AudioBufferSourceNode instances before creating new ones.

Issues fixed:
- Old bufferNode.onended event handler was not removed before disconnect
- Event handler held reference to old bufferNode preventing garbage collection
- Rapid play/pause cycles accumulated orphaned buffer nodes in memory
- Now explicitly sets onended to null before disconnecting

The bufferNode cleanup now properly removes event handlers first, allowing
the old buffer nodes to be garbage collected and preventing memory leaks
during frequent seeking or playback control changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The virtualAppend() method had memory leaks from improper subscription
management and duplicate subscription tracking.

Issues fixed:
- Removed duplicate subscription: was pushing both once() return value
  and the actual unsubscribe function to subscriptions array
- Separated cleanup logic: region.once('remove') now properly handles
  unsubscribing from wavesurfer events
- Added comment clarifying that region check prevents orphaned subscriptions
  if region is removed before setTimeout executes
- renderIfVisible closure no longer accumulates in subscriptions array

The virtualAppend method now properly tracks subscriptions and cleans them
up when regions are removed, preventing memory leaks from orphaned event
listeners on wavesurfer instances.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The loadAudio() method had issues with concurrent loads and unnecessary
defensive coding that suggested a potential race condition.

Issues fixed:
- Removed unnecessary optional chaining on freshly created AbortController
- Added explicit abort() call for previous fetch before starting new load
- Reset abortController to null after aborting to ensure clean state
- Prevents multiple concurrent fetches from interfering with each other

When load() is called while a fetch is in progress, the previous fetch
is now properly aborted before starting the new one, ensuring only one
fetch operation is active at a time and preventing race conditions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The setOptions() method would re-initialize drag handlers without
checking if they were already set up, causing duplicate handlers
to accumulate.

Issues fixed:
- Added dragUnsubscribe property to track if drag is initialized
- initDrag() now returns early if drag is already set up
- Prevents multiple drag handlers from responding to same event
- Avoids memory leak from duplicate subscriptions accumulating

When setOptions() is called multiple times with dragToSeek enabled,
only one set of drag handlers will be active, preventing unpredictable
behavior and memory leaks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings October 1, 2025 12:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR addresses 6 critical memory leaks and resource management issues to improve stability during long-running sessions and prevent memory accumulation.

  • Replaced event-based timer loop with direct requestAnimationFrame to eliminate infinite listener growth
  • Fixed EventEmitter once() method to prevent duplicate listener accumulation
  • Added proper cleanup for WebAudio buffer nodes and AbortController race conditions
  • Improved drag handler initialization tracking and region subscription management

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/timer.ts Replaces event-based tick loop with requestAnimationFrame to prevent memory leaks
src/event-emitter.ts Fixes once() method to use single wrapper function instead of recursive subscription
src/webaudio.ts Adds proper cleanup of buffer node event handlers before disconnect
src/wavesurfer.ts Prevents AbortController race conditions and removes unnecessary optional chaining
src/renderer.ts Tracks drag initialization state to prevent duplicate handler setup
src/plugins/regions.ts Separates subscription cleanup logic and removes duplicate tracking

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@cloudflare-workers-and-pages
Copy link

Deploying wavesurfer-js with  Cloudflare Pages  Cloudflare Pages

Latest commit: a731007
Status: ✅  Deploy successful!
Preview URL: https://5bb40ccf.wavesurfer-js.pages.dev
Branch Preview URL: https://fix-memory-leaks-combined.wavesurfer-js.pages.dev

View logs

@katspaugh katspaugh merged commit 0fba2ad into main Oct 1, 2025
7 checks passed
@katspaugh katspaugh deleted the fix/memory-leaks-combined branch October 1, 2025 12:54
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.

2 participants