As it turns out, cloning HTML5 Audio() elements is indeed expensive.
I noticed in Safari that I was getting some pretty severe jank (lag) when firing the machine gun in this web-based game prototype, so I started digging.
It wasn't DOM manipulation, className changes or node removal - turned out it was cloned audio resources being freed from memory after playing, and garbage allocation interrupting the game animation pretty severely in Safari (and Webkit nightly) every so often.
Using standard HTML5 audio, you can't do "multi-shot" - each audio object is single-fire and monophonic. Thus when a machine gun fires, SM2 clones the Audio() instance to make a new object that can play independently, with its own timeline and events.
These screenshots are from Chrome DevTools. Interestingly, Chrome was less-affected by this issue despite having notably-larger GC events as well. I'm not sure how long Safari was taking, but it felt like up to 1 second in Safari 6 in some cases.
In the first case, the GC event is for 9.3 MB and takes 25 msec - blowing the 16 msec / 60fps ideal framerate budget.
In the case where audio is muted and inactive, the first GC event to happen is for 3.6 MB and takes 1.9 msec.
As Mythbusters' Adam Savage would say, "Well there's your problem!"
The solution to this is to simply create a pool of sound objects and rotate through them, if the play() rate is such that the sounds would need to overlap. While this will consume more memory (if not shared/reused by the browser) up front, it should prevent a lot of dynamic memory allocation and resulting expensive garbage collection events.
In fact, most sounds (like machine gun fire) are short enough that multi-shot is not even needed.
Maybe those guys were right about writing JavaScript more like C++ after all, static and all that.
As it turns out, cloning HTML5 Audio() elements is indeed expensive.
I noticed in Safari that I was getting some pretty severe jank (lag) when firing the machine gun in this web-based game prototype, so I started digging.
It wasn't DOM manipulation, className changes or node removal - turned out it was cloned audio resources being freed from memory after playing, and garbage allocation interrupting the game animation pretty severely in Safari (and Webkit nightly) every so often.
Using standard HTML5 audio, you can't do "multi-shot" - each audio object is single-fire and monophonic. Thus when a machine gun fires, SM2 clones the Audio() instance to make a new object that can play independently, with its own timeline and events.
These screenshots are from Chrome DevTools. Interestingly, Chrome was less-affected by this issue despite having notably-larger GC events as well. I'm not sure how long Safari was taking, but it felt like up to 1 second in Safari 6 in some cases.
In the first case, the GC event is for 9.3 MB and takes 25 msec - blowing the 16 msec / 60fps ideal framerate budget.
In the case where audio is muted and inactive, the first GC event to happen is for 3.6 MB and takes 1.9 msec.
As Mythbusters' Adam Savage would say, "Well there's your problem!"
The solution to this is to simply create a pool of sound objects and rotate through them, if the play() rate is such that the sounds would need to overlap. While this will consume more memory (if not shared/reused by the browser) up front, it should prevent a lot of dynamic memory allocation and resulting expensive garbage collection events.
In fact, most sounds (like machine gun fire) are short enough that multi-shot is not even needed.
Maybe those guys were right about writing JavaScript more like C++ after all, static and all that.