Demystifying JavaScript Memory Management – Why should I care? – Checklist to detect memory leaks



Demystifying JavaScript Memory Management – Why should I care? – Checklist to detect memory leaks

0 1


js-memory-management

Slides for 'Demystifying JavaScript memory management' talk at #TEFCON2014

On Github jalopez / js-memory-management

Demystifying JavaScript Memory Management

#TEFCON2014

Javier López Pardo - @javilp

http://jalopez.github.io/js-memory-management

Key questions

Do I know how much memory my JS app is it consuming? Has it any memory leak? Do I know how to look for the answers of questions 1 and 2?

Tip: We will use Chrome Dev Tools. They are awesome!

What is this all about?

  • Understand JS memory allocation and GC
  • Find and kill memory leaks
  • Advanced memory techniques

Why should I care?

  • Client side apps in "low-end" devices
    • Mobile (Including Firefox OS)
    • Chromebooks
    • Smart TV Apps
    • Set-top boxes
  • Server side applications

Does it worth the effort?

Which kind of app do I have?

  • Single page applications
  • HTML5 Games
  • Apps for HW limited environments
  • Long-life apps (server side...)

For sure

  • Form validation
  • Fancy effects

No way

Memory vs. Performance vs. Development Cost

"Early optimization is the root of all evil" D. Knuth

A real world scenario(Hybrid TV)

  • Satellite + OTT Set-top Box
  • UI: SVG + JS (~ 80 KLOCs)
  • 12Mb Heap in Browser for UI
  • Memory footprint evolution across versions6 Mb - 4.5 Mb - 3.9 Mb
  • 10+ Memory leaks detected

Understanding memory management

Basic concepts

  • Heap memory
  • Garbage collection
  • Memory leak
  • Memory graph
"The heap area is the dynamic memory pool where objects are allocated when they are created by a program" "The garbage collector is the process that frees a memory area when the objects allocated in it, are no longer retained (being used) by the program" "A memory leak is a memory area that cannot be collected even when it will be no longer actively used" "The memory graph is the visual representation of how memory is organized, showing variable values starting in the root node until leaf nodes"

JavaScript Memory Graph. Allocating memory

// window
var a = 3;
					    
var myObject = {
    prop1: a, // It's a copy
    prop2: 'A string'
};
					    
var myArray = [
    myObject, // By reference
    new Date()
];
					    

Removing a value

myArray = null;
					    
myObject = null;
					    

Garbage collection

myArray = null;
					    
myObject.prop1 = null;
// delete myObj.prop1;
					    
myObject = null;
					    

When can the GC collect a node?

"A memory area can be freed when there is no active reference to it" Local variables of a function when it is executed and returns Any object whose all references (variables pointing to) have been nullified (or deleted) Closure scope variables when the function references disappear
  • Event listeners when they are unbounded
DOM Nodes where they are not in the DOM tree and there is no variable pointing to it

What is a memory leak?

"A leak is a memory area that will be never freed by the GC, even when there won't be further active access to it"
  • Circular references in objects/functions
  • "Global" variables (lists, etc.) never emptied
  • DOM nodes not removed from JS objects

Finding and killing memory leaks

Checklist to detect memory leaks

Check initial memory state (Memory snapshot) Execute the suspicious action that should not increase memory Check memory again If memory is significantly higher, There is a leak Use Profiles and Timeline to track what is happening
  • Amount of DOM nodes
  • Retained memory

Tips for working with Chrome Dev Tools

  • Disable add-ons (or use Private browsing)
  • Clean console (disable logs if possible)

Show me the code!

What's wrong with this?

function testLeak() {
    myArray = [];

    for (var i = 0; i < 1000000; i++) {
        myArray.push(i.toString());
    }
    return myArray[0];
}

function runTest() {
    testLeak();
}
                    
function testLeak() {
    var myArray = []; // Don't forget the var here

    for (var i = 0; i < 1000000; i++) {
        myArray.push(i.toString());
    }
    return myArray[0];
}

function runTest() {
    testLeak();
}
                    

DON'T USE GLOBAL VARIABLES!

[show in browser]

Finding the root of a memory leak

The two steps technique

Init in clean state Take a heap snapshot Execute a sequence of steps and come back to initial state Take another snapshot Check differences between Snapshot 1 and Snapshot 2

Understanding the results

  • Distance: number of jumps from the root (window)
  • Shallow size: size of the object itself
  • Retained size: size of the object plus the objects it is referencing
  • Retainers: list of objects retaining this object/node
    • Yellow Node has a JavaScript reference
    • Red Node referenced by a yellow node

Finding the root of a memory leak

Record heap allocations

Start recording a heap allocation Execute suspicious actions Analyze results in timeline

Blue lines are allocated memory

Grey lines are freed memory

And what about Node.js?

Use heapdump

var heapdump = require('heapdump');

heapdump.writeSnapshot('/tmp/step1.heapsnapshot');
executeLeakyAction();
heapdump.writeSnapshot('/tmp/step2.heapsnapshot');
                    

Load *.heapsnapshot in Chrome Profile

Common memory leaks

1. Global variables

See previous slide...

Not only global, but also long-live objects:

  • Module pattern
  • Memoized functions
  • Caches

2. DOM Leaks

var ToDoList = function() {
    this._items = [];
    this._itemIndex = 0;

    this._mainNode = document.getElementById('main');
};

ToDoList.prototype.addItem = function() {
    var li = document.createElement('li');
    li.innerHTML = 'Item ' + this._itemIndex++;
    this._items.push(li);

    this._mainNode.appendChild(li);
};

ToDoList.prototype.removeAll = function() {
    for (var i = 0, len = this._items.length; i < len; i++) {
        this._mainNode.removeChild(this._items[i]);
    }
};
                    

[show in browser]

Remove all references to DOM nodes

3. Closures

function testLeak() {
    var myArray = [],
        mySecondArray = [1, 2, 3, 4],
        anotherObject = {a: 'b'};

    for (var i = 0; i < 1000000; i++) {
        myArray.push(i.toString());
    }
    return function leakClosure() {
        return myArray;
    };
}

var closure;

function createMemory() {
    closure = testLeak();
}
function freeMemory() {
    closure = null;
}
                    

[show in browser]

Closures retain scope variables

3.1 Event listeners

function registerEvent() {
    var text = new Array(10000000).join('x')
    var node = document.getElementById('div');

    node.addEventListener('click', function() {
        return text;
    });
};
                    

Remove event handlers when no longer needed

3.2. Closures inside objects / Timers

var object;

function createMemory() {
    object = {
        prop: new Array(10000000).join('x'),
        run: function() {
            var that = this;

            setTimeout(function() {
                that.run();
            }, 10000);
        }
    };
    object.run();
}

function freeMemory() {
    object = null;
}
                    

[show in browser]

Take care of "that" in closure scope.

Advanced memory management

v8 Hidden classes. Fast objects

  • Chrome "compile" JS and create faster hidden classes that represent app objects
  • When the structure of an object is changed by deleting properties, the object becomes slow, and takes much more memory

Fast object

var fastObject = {
    prop1: 'string',
    prop2: new Date()
};
// Fast operation
fastObject.prop1 = null;
                        

Slow object

var slowObject = {
    prop1: 'string',
    prop2: new Date()
};
// Slow operation
delete slowObject.prop1;
                        

See it in action

The cost of Garbage Collection

  • Every time the heap area is exhausted the garbage collector frees memory
  • Every memory allocation makes next GC closer
  • GC can take milliseconds to run: affects smoothness of applications
    • Higher latency
    • Less FPS (Be careful game developers)

Introducing Memory Pools

  • Use a memory pool for keep objects always in memory and avoid GC
  • Increase setup time but app runs smoother
  • Take care of consuming too much memory
// It creates 1000 objects and store them in a local cache
var pool = new SimpleMemoryPool({prop1: null, prop2: null}, 1000);

// Get an object from the pool, and assign the desired values
var myObject = pool.getInstance('an string', new Date());
// myObject = {prop1: 'an string', prop2: new Date()}


// When I don't need the object anymore, release it to the pool again
pool.release(myObject);
// prop1 and prop2 are nullified and object goes back to the pool

                        

Automatizing memory tests. Logging your memory

Use Chrome performance.memory API

  • jsHeapSizeLimit: maximum heap size
  • usedJSHeapSize: current memory allocated
  • totalJSHeapSize: allocated heap, including free space

Enable memory logging with --enable-precise-memory-info

Tip: Automatize your memory tests (Selenium...)

Homework

  • ES6 WeakMaps
  • V8 Young generation objects vs. Old generation
  • JS execution loop, DOM layout and reflows

Useful tips when developing a big JS application

  • Don't rely too much on Internet libraries
  • Don't keep DOM references in code when don't need them
  • The less global state, the better
  • Take care about scope
  • Use data caches
    • ...but take care of the amount of memory (MRU caching)

References

Thank you!

Questions?