The JavaScript Event Loop

Presented by @tlhunter@mastodon.social

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