Skip to content

Fix: Additional memory leaks and performance issues in plugins#4211

Merged
katspaugh merged 10 commits intomainfrom
fix/additional-memory-leaks-and-bugs
Oct 1, 2025
Merged

Fix: Additional memory leaks and performance issues in plugins#4211
katspaugh merged 10 commits intomainfrom
fix/additional-memory-leaks-and-bugs

Conversation

@katspaugh
Copy link
Owner

Summary

This PR fixes 15 additional memory leaks, performance issues, and bugs discovered in a follow-up analysis of the codebase. These issues primarily affect plugins and could cause memory leaks in long-running applications.

Issues Fixed

Critical Performance Issues

  • Timeline Plugin (Add API for events like click, ready etc #19): Refactored to use single scroll listener instead of one per notch
    • Previously created N listeners for N notches (e.g., 60 for a 60-second audio)
    • Significant improvement for scroll performance with many timeline notches

Memory Leaks Fixed

Safety & Validation Improvements

Code Quality Improvements

Testing Recommendations

  1. Create/destroy plugins repeatedly and monitor event listener counts
  2. Test timeline with very long audio files (hours)
  3. Test envelope plugin on touch devices
  4. Load files rapidly to verify no race conditions
  5. Monitor memory usage during extended use

Impact

These fixes will prevent memory leaks in applications that:

  • Create and destroy plugins frequently
  • Use timeline with long audio files
  • Run for extended periods
  • Use touch devices with envelope plugin

🤖 Generated with Claude Code

- Clear throttleTimeout in EnvelopePlugin destroy to prevent callbacks after destruction
- Store and remove dblclick listener from Polyline SVG element
- Store and remove touch event listeners (touchstart, touchmove, touchend)
- Clear pressTimer on Polyline destroy to prevent pending callbacks

Fixes memory leaks in envelope plugin (issues #16, #21, #22)
- Create unsubscribe functions immediately after attaching listeners
- Store only pointer coordinates instead of entire PointerEvent object
- Properly track zoom and scroll event unsubscribers

Fixes issues #17 and #24
- Listener was added to container but removed from wrapper
- Now correctly removes listener from container element

Fixes issue #18
- Refactored to use one scroll listener instead of one per notch
- Store notch metadata in a Map for batch visibility updates
- Significantly improves scroll performance with many notches
- Previously created N listeners for N notches (e.g., 60 for a 60-second audio)

Fixes issue #19
- Clear container reference on destroy to aid garbage collection
- Add isInitializing flag to prevent concurrent initMinimap() calls
- Ensures proper cleanup and prevents orphaned WaveSurfer instances

Fixes issues #20 and #25
- Use explicit try/finally for AudioContext cleanup in decode()
- Add input validation to createBuffer() to prevent invalid AudioBuffer
- Prevent division by zero and undefined channel data errors

Fixes issues #23 and #29
- Rename 'reject' variable to 'rejectFn' for clarity
- Clear both timeout and reject function in onClear
- Add explicit typing and comments for better maintainability
- Ensure proper cleanup of pending delays

Fixes issue #26
- Add documentation noting WebAudioPlayer doesn't manage blob URLs
- Clarify that Player class handles blob URL lifecycle
- Helps prevent memory leaks when using WebAudioPlayer standalone

Addresses issue #27
- Call media.load() before media.remove() instead of after
- Ensures media element is properly reset before DOM removal
- More logical cleanup sequence

Fixes issue #30
Copilot AI review requested due to automatic review settings October 1, 2025 13:29
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 15 memory leaks, performance issues, and bugs primarily affecting plugins in long-running applications. The changes focus on proper cleanup of event listeners, timeouts, and references to prevent memory accumulation.

  • Timeline plugin refactored to use single scroll listener instead of one per notch for significant performance improvement
  • Memory leaks fixed in envelope, hover, zoom, and minimap plugins through proper event listener cleanup
  • Safety improvements added to decoder with input validation and proper AudioContext cleanup

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/webaudio.ts Added documentation about blob URL management responsibility
src/renderer.ts Improved createDelay logic with clearer variable naming and cleanup
src/plugins/zoom.ts Fixed wheel listener removal from correct element
src/plugins/timeline.ts Refactored to use single scroll listener and batch notch updates
src/plugins/minimap.ts Added race condition prevention and container cleanup
src/plugins/hover.ts Fixed event listener cleanup and reduced memory usage
src/plugins/envelope.ts Added comprehensive cleanup for touch events and timers
src/player.ts Reordered destroy operations for logical cleanup sequence
src/decoder.ts Added input validation and proper AudioContext cleanup

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

if (this.lastPointerMove) this.onPointerMove(this.lastPointerMove)
if (this.lastPointerPosition) {
// Recreate a minimal PointerEvent-like object
this.onPointerMove(this.lastPointerPosition as PointerEvent)
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

The type assertion as PointerEvent is unsafe since lastPointerPosition only contains clientX and clientY properties. Consider creating a proper interface or passing the required properties explicitly to avoid potential runtime errors if onPointerMove expects other PointerEvent properties.

Copilot uses AI. Check for mistakes.
src/decoder.ts Outdated
Comment on lines 40 to 46
if (!channelData[0] || channelData[0].length === 0) {
throw new Error('channelData must contain non-empty channel arrays')
}

// If a single array of numbers is passed, make it an array of arrays
if (typeof channelData[0] === 'number') channelData = [channelData as unknown as number[]]

Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

This validation occurs after the type coercion check on line 45, but the validation assumes channelData[0] exists as an array. If channelData[0] is a number (which gets converted to an array later), this check will incorrectly fail since numbers don't have a length property. Move this validation after the type coercion logic.

Suggested change
if (!channelData[0] || channelData[0].length === 0) {
throw new Error('channelData must contain non-empty channel arrays')
}
// If a single array of numbers is passed, make it an array of arrays
if (typeof channelData[0] === 'number') channelData = [channelData as unknown as number[]]
// If a single array of numbers is passed, make it an array of arrays
if (typeof channelData[0] === 'number') channelData = [channelData as unknown as number[]]
if (!channelData[0] || channelData[0].length === 0) {
throw new Error('channelData must contain non-empty channel arrays')
}

Copilot uses AI. Check for mistakes.
if (!this.wavesurfer) return
const width = element.clientWidth
const isVisible = start >= scrollLeft && start + width < scrollRight
const notchData = this.notchElements.get(element)!
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

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

Using the non-null assertion operator ! without a prior existence check is risky. Consider adding a guard clause to handle the case where the element is not found in the map to prevent potential runtime errors.

Suggested change
const notchData = this.notchElements.get(element)!
const notchData = this.notchElements.get(element)
if (!notchData) {
// If the element is not found in the map, do not proceed
return
}

Copilot uses AI. Check for mistakes.
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 1, 2025

Deploying wavesurfer-js with  Cloudflare Pages  Cloudflare Pages

Latest commit: ae548c9
Status: ✅  Deploy successful!
Preview URL: https://33a0bd2d.wavesurfer-js.pages.dev
Branch Preview URL: https://fix-additional-memory-leaks.wavesurfer-js.pages.dev

View logs

- Validate channel length after converting flat array to array of arrays
- Prevents false validation errors for valid flat array inputs
- Fixes failing e2e tests for pre-decoded waveform and blob loading
@katspaugh katspaugh merged commit 6727a80 into main Oct 1, 2025
6 checks passed
@katspaugh katspaugh deleted the fix/additional-memory-leaks-and-bugs branch October 1, 2025 13:47
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