Search code examples
javascriptmemorymemory-leaksweb-audio-api

Javascript timer memory leak


I've been working on a canvas game and been running into some problems involving a memory leak. I thought the problem was to do with rendering and removing entities, but I ran the code without rendering any and it looks like the audio scheduling object is causing a leak by itself. The problem causes the audio to start crackling and cut out after a while. The game still renders but the audio stops - I also noticed the character's shooting becomes much slower (shoot timing is scheduled along with the notes in the scheduler function).

Link to game

The code I used to deal with the audio is from the A Tale of Two Clocks' tutorial. When I ran the code for the metronome on chrome and did a timeline recording the heap allocation went up.

Why is this code causing a memory leak? It looks fine to me. Timer ID is set to Null?

Image 1: Heap timeline of metronome by itself

https://i.sstatic.net/aAB3r.png

Image 2: Timeline of my app running just the metronome function (no game entities)

http://i.imgur.com/vGCbI1H.png

Image 3: My app running normally

https://i.sstatic.net/czE2v.png

This is the code

function init(){
  var container = document.createElement( 'div' );
  // CREATE CANVAS ... ...
  audioContext = new AudioContext();
  requestAnimFrame(draw);    

  timerWorker = new Worker("js/metronomeworker.js");
  timerWorker.onmessage = function(e) {
    if (e.data == "tick") {
      // console.log("tick!");
      scheduler();
    }
    else {
      console.log("message: " + e.data);
    }
  };
  timerWorker.postMessage({"interval":lookahead});
}

function nextNote() {
  // Advance current note and time by a 16th note...
  var secondsPerBeat = 60.0 / tempo;    // Notice this picks up the CURRENT 
                                        // tempo value to calculate beat length.
  nextNoteTime += 0.25 * secondsPerBeat;    // Add beat length to last beat time

  current16thNote++;    // Advance the beat number, wrap to zero
  if (current16thNote == 16) {
      current16thNote = 0;
  }
}

function scheduleNote( beatNumber, time ) {
  // push the note on the queue, even if we're not playing.
  notesInQueue.push( { note: beatNumber, time: time } );

  if ( (noteResolution==1) && (beatNumber%2))
    return; // we're not playing non-8th 16th notes
  if ( (noteResolution==2) && (beatNumber%4))
    return; // we're not playing non-quarter 8th notes

  // create an oscillator    //   create sample
  var osc = audioContext.createOscillator();
  osc.connect( audioContext.destination );
  if (beatNumber % 16 === 0)    // beat 0 == low pitch
    osc.frequency.value = 880.0;
  else if (beatNumber % 4 === 0 )    // quarter notes = medium pitch
    osc.frequency.value = 440.0;
  else                        // other 16th notes = high pitch
    osc.frequency.value = 220.0;

  osc.start( time );              //sound.play(time)
  osc.stop( time + noteLength );  //   "      "
}

function scheduler() {
  // while there are notes that will need to play before the next      interval, 
  // schedule them and advance the pointer.
  while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
    scheduleNote( current16thNote, nextNoteTime );
    nextNote();
  }
}

function play() {
isPlaying = !isPlaying;

if (isPlaying) { // start playing
    current16thNote = 0;
    nextNoteTime = audioContext.currentTime;
    timerWorker.postMessage("start");
    return "stop";
} else {
    timerWorker.postMessage("stop");
    return "play";
}
}

Metronome.js:

var timerID=null;
var interval=100;

self.onmessage=function(e){
  if (e.data=="start") {
    console.log("starting");
    timerID=setInterval(function(){postMessage("tick");},interval)
  }
  else if (e.data.interval) {
    console.log("setting interval");
    interval=e.data.interval;
    console.log("interval="+interval);
    if (timerID) {
      clearInterval(timerID);
      timerID=setInterval(function(){postMessage("tick");},interval)
    }
  }
  else if (e.data=="stop") {
    console.log("stopping");
    clearInterval(timerID);
    timerID=null;
  }
};

How I schedule sounds (and shooting) inside scheduleNote() :

if (beatNumber % 4 === 0) {
    playSound(samplebb[0], gainNode1);
 }
if (planet1play === true) {
    if (barNumber % 4 === 0)
        if (current16thNote % 1 === 0) {
            playSound(samplebb[26], planet1gain);
        }
}
if (shootx) {
    //  Weapon 1
    if (gun === 0) {
        if (beatNumber === 2 || beatNumber === 6 || beatNumber === 10 || beatNumber === 14) {
            shoot(bulletX, bulletY);
            playSound(samplebb[3], gainNode2);
        }
    }

Update

The Audio is still having problems even if I run the game without rendering or updating anything Here It is worse on slower machines.

No idea why this is happening, some kind of audio buffer issue? Anyone have any ideas?


Solution

  • There is a risk of running several setIntervals here if start command is called in succession. If they are they will pile up and could explain why the memory is increasing.

    I would suggest the following changes. Without you could simply check if timerID existed in the start method, but centralizing the methods will help keep track. clearInterval() can be called with a null argument with no consequences other that it will ignore it.

    So in essence:

    var timerID = null;
    var interval = 100;
    
    function tickBack() {           // share callback for timer
        postMessage("tick")
    }
    
    function startTimer() {             // centralize start method
        stopTimer();                    // can be called with no consequence even if id=null
        timerID = setInterval(tickBack, interval)
    }
    
    function stopTimer() {              // centralize stop method
        clearInterval(timerID);
        timerID = null;
    }
    
    onmessage = function(e){
    
      if (e.data === "start") {
        startTimer();
      }
      else if (e.data === "stop") {
        stopTimer()
      }
      else if (e.data.interval) {
        interval = e.data.interval;
        if (timerID) startTimer();
      }
    };