You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Switching tracks mid-playback leaks the engine that was playing #601
Loading a new track while the current one is still playing leaves the old audio engine permanently reachable, along with every decoded audio buffer it was holding. Repeat it through a listening session and the browser tab's memory only ever goes up.
Why it never recovers
destroyPlayer() (static/js/player.js) tears the engine down by calling audioEngine.destroy() directly. It does not pause first, and it has no reason to: the engine is being thrown away.
The problem is what destroy() leaves behind. Each engine registers a visibilitychange listener on document, and document outlives every engine that will ever be created. So the listener holds the engine's whole closure, which holds tracks, which holds an AudioBuffer per stem. Nothing is ever removed, so nothing can be collected.
The full-decode engine had a second half to this: destroy() never cleared playing. A dead engine therefore reported isPlaying() === true forever, and the leaked listener still believed it had work to do.
Why it is invisible
Nothing fails. Playback of the new track is completely normal. The only symptom is memory, and only after several switches, which is why it survives any amount of ordinary use and any amount of testing.
A listener per engine is the wrong shape regardless of cleanup. Engines are created and destroyed on every track change; the thing being listened for is global.
Measuring this needs care. The JS heap does not count AudioBuffer backing memory at all, so heap numbers can look flat while hundreds of MB leak. Counting live loop registrations is the honest instrument.
Tick loops now deregister on dispose, and the statechange listener comes off the context too, so a torn-down engine is no longer reachable from document and its decoded buffers can be collected. audioEngine.destroy() also clears playing, which it never did.
Verified with a browser driving six mid-playback track switches: live tick loops stayed flat at 1. With dispose deliberately broken the same harness reported 2 to 7, one per switch, which is how I know the measurement can actually fail.
What happens
Loading a new track while the current one is still playing leaves the old audio engine permanently reachable, along with every decoded audio buffer it was holding. Repeat it through a listening session and the browser tab's memory only ever goes up.
Why it never recovers
destroyPlayer()(static/js/player.js) tears the engine down by callingaudioEngine.destroy()directly. It does not pause first, and it has no reason to: the engine is being thrown away.The problem is what
destroy()leaves behind. Each engine registers avisibilitychangelistener ondocument, anddocumentoutlives every engine that will ever be created. So the listener holds the engine's whole closure, which holdstracks, which holds anAudioBufferper stem. Nothing is ever removed, so nothing can be collected.The full-decode engine had a second half to this:
destroy()never clearedplaying. A dead engine therefore reportedisPlaying() === trueforever, and the leaked listener still believed it had work to do.Why it is invisible
Nothing fails. Playback of the new track is completely normal. The only symptom is memory, and only after several switches, which is why it survives any amount of ordinary use and any amount of testing.
Constraints for anyone fixing this
AudioBufferbacking memory at all, so heap numbers can look flat while hundreds of MB leak. Counting live loop registrations is the honest instrument.Found while fixing #600.