Automated Memory Leak Testing in the Browser

Large single page web applications have a disadvantage over multi-page applications in that no page reload occurs. This means memory leaks are able to cause more performance problems in single page web applications as the lack of page reloads will not clear the javascript heap. While catching these leaks can be done by manually memory profiling, I will show you an automated test that will give a glimpse at whether memory leaks may be occuring.

The actual test is pretty simple though it requires Chrome to run. This is because Chrome comes with some nifty flags that let in browser tests do things other browsers can't.

  1. Chrome is the only browser that has the ability to read precise memory usage via javascript. This is enabled by the --enable-precise-memory-info flag.
  2. Chrome also is the only browser that allows you to trigger the garbage collector from javascript. This is enabled by the custom javascript flag --js-flags="--expose-gc"

Where I currently work, we use the Karma test runner for testing so setting up different browsers is as easy as installing an npm module and adding the browser to the build with the given flags.

After adding Chrome to your test runner, you will want to add the actual memory test. The memory test consists of a few parts.

  • A header that guards against browsers that don't support the features we need.
const isDef = (o)=>o !== undefined && o !== null;
if (!isDef(window.performance) || !isDef(window.performance.memory)) {
    console.log("Unsupported environment, window.performance.memory is unavailable");
    this.skip(); //Skips test (in Chai)
    return;
}
if (typeof window.gc !== "function") {
    console.log("Unsupported environment, window.gc is unavailable");
    this.skip();
    return;
}
  • A function for getting a memory profile
const getMemoryProfile = ()=>{ 
    window.gc(); //Trigger GC to get precise used memory reading
    return window.performance.memory.usedJSHeapSize; //Return used memory
};
  • An object that tracks memory profiles over time (optional though useful)
const profile = {
    samples: [],
    diffs: [],
    averageUsage: 0,
    averageChange: 0,
    //Collects a sample of memory and updates all the values in the
    //profile object
    sample() {
        const runningAverage = (arr, newVal, oldAvg)=>{
            return ((oldAvg * (arr.length-1) + newVal) / arr.length);
        };

        let newSample = getMemoryProfile();
        this.samples.push(newSample);
        this.averageUsage = runningAverage(this.samples, newSample, this.averageUsage);
        if(sampleLen >= 2) {
            let newDiff = this.samples[sampleLen - 1] - this.samples[sampleLen - 2];
            this.diffs.push(newDiff);
            this.averageChange = runningAverage(this.diffs, newDiff, this.averageChange);
        }
    }
};
  • The portion of your application you want to profile between calls to profile.sample(). In my case, this was the loading of a particularly heavy WebGL scene whereby a 50MB+ model was loaded in addition to many other operations where it would then be "disposed" of and all references to created objects broken (or we hope!).
profile.sample();
//Your test code here!
profile.sample();
  • The test assertions that are specific to the memory requirements of your application. Some sample assertions are given below (in this case using the chai expect framework)
const inMB = (n)=>n/1000000;

//Check average change in memory samples to not be over 10MB
expect(inMB(profile.averageChange).to.be.at.most(10);

//Check the final memory usage against the first usage, there should be little change if everything was properly deallocated
expect(inMB(getMemoryProfile())).to.be.at.most(profile.samples[0] + 0.25);

This test has saved me a couple of times though isn't bullet proof. Chrome and Firefox are very different beasts when it comes to memory management and, with specific "experimental" technologies like WebGL (come on, we already have WebGL2!), Firefox really likes to leak. This is the point where you start using the memory profiling tools native to your browser (Firefox profiler and Chrome Profiler). I prefer Chrome's toolset just because it gives you so much more control but Firefox's will probably get there with time.