The JavaScript Event Loop

Presented by @tlhunter

Distributed Systems with Node.js
  • Distributed Systems with Node.js: bit.ly/34SHToF
  • Part I

    JavaScript Overview

    JavaScript is Single-Threaded

    • Makes use of a single CPU thread (think CPU core)
    • Nothing done inside JavaScript is “concurrent”
    • Easier to reason about than Multi-Threaded
    • Unfortunate side-effects such as Scroll Jank™

    Technical Implementation

    • Stack:
      • Function calls, context information
      • As functions call functions, add frames to stack
    • Queue (Many Languages Don't Have):
      • Work scheduled to be added to stack
      • E.g. setTimeout() and setInterval()
    • Heap:
      • “Chaotic” collection of objects, context vars, etc.
      • Garbage Collection cleans items from heap
    • Event Handlers:
      • Can add items to queue in the future

    Queue/Stack/Heap Diagram

    The Event Loop is named after repeatedly taking work from the queue and making new stacks.

    Image Credit: Mozilla Developer Network:
    http://mzl.la/Y5Dh2x

    Example Code-run

                
                  function run() {
                    console.log("Adding code to the stack");
    
                    setTimeout(function c() { // c() Added somewhere in Heap
                      console.log("c() Running next code from queue");
                    }, 0);
    
                    function a(x) { // a() Added somewhere in Heap
                      console.log("a() frame added to stack");
                      b(x);
                      console.log("a() frame removed from stack");
                    }
    
                    function b(y) { // b() Added somewhere in Heap
                      console.log("b() frame added to stack");
                      console.log("Value passed in is " + y);
                      console.log("b() frame removed from stack");
                    }
    
                    a(42);
    
                    console.log("Ending work for this stack");
                  }
                
              

    Code-run Visualized with Dev Tools

    • This type of visualization is a Flame Graph
    • Interactive Demo: bit.ly/2kF3TMh
    ...

    Interview Question

    • In what order are the letters output?
    • Extra Credit: How long does each letter take?
                
    
    setTimeout(function() { console.log('A'); }, 0);
    
    console.log('B');
    
    setTimeout(function() { console.log('C'); }, 100);
    
    setTimeout(function() { console.log('D'); }, 0);
    
    var i = 0;
    while (i < 200000000) { // Takes ~500ms to run this loop
      var ignore = Math.sqrt(i);
      i++;
    }
    
    console.log('E');
                
              

    Part II

    I/O Considerations

    Your App is Mostly Asleep

    • Browser
      • Wait for a click to happen
      • Wait for AJAX response
    • Node.js
      • All I/O is non-blocking (libuv)
      • C++ API does the heavy lifting
      • Once I/O is complete callback is queued up

    Sequential vs Parallel

    • Classical web apps perform each I/O Sequentially
    • With an Event Loop, they can be run in Parallel
    • Most time waiting for I/O; Sequential is inefficient
    Sequential I/O
    Parallel I/O

    Why Single-Threaded Event Loops are Awesome:

    • No concurrent memory access problems
    • Usually web apps spend most time waiting on I/O
    • Easily perform I/O operations “in parallel”
      • Thanks to non-blocking APIs
    • Long running apps, don’t need separate web servers

    Why Single-Threaded Event Loops aren’t Awesome:

    • CPU intensive work will block your process
    • Memory leaks can happen
    • A single JavaScript instance cannot fully utilize CPU

    Part III

    Breaking up heavy workloads

    ...

    Single Stack: Freeze Rendering

                
                  var LIMIT = 200000;
    
                  function drawMany() {
                    for (var i = 0; i < LIMIT; i++) {
                      output.appendChild(document.createElement('div'));
                    }
                  }
                
              
    ...

    Queueing: Allows Rendering

                
                  var LIMIT = 200000;
                  var CHUNK = 1000;
    
                  function drawFew(start, callback) {
                    for (var i = 0; i < CHUNK; i++) {
                      output.appendChild(document.createElement('div'));
                    }
    
                    if (start >= LIMIT) return callback();
    
                    setTimeout(function() {
                      drawFew(start + CHUNK, callback);
                    }, 0);
                  }
                
              
    ...
    ...

    Web Workers

    • Separate JavaScript instance, has its own Event Loop
    • Message Passing via JSON structures
    • No deadlocks or race conditions, working with “copies”
    • Can't touch the DOM, tho AJAX and WebSockets work
                
                  // main.js
                  var worker = new Worker('task.js');
                  worker.postMessage({iterations: 5000000000});
                  worker.onmessage = function(e) { console.log(e.data); };
    
                  // task.js
                  onmessage = function(e) {
                    var pi = 0, n = 1;
                    for (i = 0; i <= e.data.iterations; i++) {
                      pi = pi + (4/n) - (4 / (n + 2)); n += 4;
                    }
                    postMessage(pi);
                  };
                
              

    Node.js Load Balancing

    • Route requests between multiple application instances
                
    var cluster = require('cluster');
    var http = require('http');
    
    if (cluster.isMaster) {
      cluster.fork(); cluster.fork(); cluster.fork();
    } else {
      http.createServer(function(req, res) {
        res.end("Hello World from: " + process.pid);
      }).listen(80);
    }
                
              

    Conclusion

    • Browser:
      • Spend < 16ms in each stack
        • Anything slower will be noticed by the human eye
      • Split heavy DOM workloads, add to queue
      • Offload CPU intensive work onto a Web Worker
    • Node.js:
      • Use a load balancer, run multiple app instances
      • The cluster module makes this easy

    Distributed Systems with Node.js: bit.ly/34SHToF