This is an effort to formalize & visualize "asynchronous context" in Node.js applications.
The content here is a "simple summary" of more in-depth work, namely
- A DLS 2017 paper that formally defines semantics of async execution in javascript
- A "translation" of the above concepts, without the academic assumptions & formalisms. (WIP)
This page is a companion to the above. The intention is to easily bring an understanding of the asynchronous execution model formally defined above to a wide audience of Node.js and Javascript developers.
Javascript is a single-threaded language, which simplifies many things. To prevent blocking IO, operations are pushed onto the background and associated with callback functions written in JavaScript. When IO operations complete, the callback is pushed onto a queue for execution by Node's "event loop". This is explained in more detail here.
While this model has many benefits, one of the key challenges is maintaing "context" when asynchronous callbacks are invoked. The papers above describe "asynchronous context" in a much more rigorous way, but for our purposes, we'll think of "asynchronous context" as the ability to answer, at any given point in program execution, "what was the path of asynchronous functions that got me here"?
One of the key challenges with "asynchronous context" is the lack of agreed upon terminology and semantics. Let's define some:
-
Execution Frame
- AnExecution Frame
is a period of program execution, defined precisely as the period of time that a special function, called aContinuation
, is executing. At a lower level of abstraction, you can think of anExecution Frame
as the period of time from when specific call frame is pushed on the stack, until that call frame is unwound off of the stack. Not all functions areContinuations
(more on that below). A specific instance of aContinuation
can be invoked multiple times; each invocation corresponds to a uniqueExecution Frame
. -
Continuation
- AContinuation
is a JavaScript function created in oneExecution Frame
and passed to a host library to be invoked later. Upon invocation, aContinuation
creates a new uniqueExecution Frame
. For example, when we callsetTimeout(function c() {}, 1000)
,setTimeout
is invoked in oneExecution Frame
, with the caller passing aContinution
(namelyc
) as a parameter. Whenc
is invoked after the timeout period, a newExecution Frame
is created, and whenc
completes, thatExecution Frame
is completed. -
Continuation Point
- Functions that accept aContinuation
as a parameter are calledContinuation Points
.Continuation Points
are determined by convention of the host. Some examples includesetTimeout
andPromise.then
. Note that not all functions that take a function as a parameter areContinuation Points
- the parameter must be invoked asynchronously. i.e., functions passed as parameters and invoked in the currentExecution Frame
are notContinuation Points
. For example,Array.prototyp.forEach
is not considered aContinuation Point
. -
Link Point
- ALink Point
is point in program execution where aContinuation Point
is invoked. This creates a logical "binding" between the currentExecution Frame
and theContinuation
passed as a parameter. We call this binding theLinking Context
. -
Ready Point
- AReady Point
is a point in program execution where a previously linkedContinuation
is made "ready" to execute. This creates a logical "binding" between the Continuation and the currentExecution Frame
. This binding is called theReady Context
(sometimes called aCausal Context
). Generally, theReady Point
always occurs at or after theLink Point
. Promises, however, are different. For promises, theReady Point
occurs when the previous promise in the promise chain is resolved.
The above definitions map nicely to a set of four events generated at runtime. These events let us track "async context":
executeBegin
- indicates the start of anExecution Frame
.link
- indicates aContinuation Point
was called andContinuation
was "pooled" for later execution.ready
- indicates aReady Point
was reached.executeEnd
- indicates the end of of anExecution Frame
.
For example, consider the code below:
console.log('starting');
Promise p = new Promise((reject, resolve) => {
setTimeout(function f1() {
console.log('resolving promise');
resolve(true);
}, 100);
}).then(function f2() {
console.log('in then');
}
Given our model, this would produce the following event stream:
```json
{"event": "executeBegin", "executeID": 0 } // main program body is starting
// starting
{"event": "link", "executeID":0, "linkID": 1} // indicates f1() was "linked" in the call to "setTimeout()"
{"event": "ready", "executeID":0, "linkID": 1, "readyID": 2}
{"event": "link", "executeID":0, "linkID": 3} // indicates f2() was "linked" in the call to "then()"
{"event": "executeEnd", "executeID": 0 } // main program body is ending
{"event": "executeBegin", "executeID": 4, "readyID":2 } // callback f1() is now starting
// resolving promise
{"event": "ready", "executeID":4, "linkID": 3, "readyID": 5} // promise p is now resolved, allowing the "then(function f2()..." to proceed
{"event": "executeEnd", "executeID": 4 } // callback f1() is ending
{"event": "executeBegin", "executeID": 6, "readyID":5 } // callback f2() is now starting
// resolving promise
{"event": "executeEnd", "executeID": 6 } // callback f1() is ending
The events above allow us to produce a Directed Acyclic Graph (DAG)
that we call the "Async Call Graph". Specifically, the executeBegin
, link
, and ready
events correspond to node & edge creation in the graph.
This all much easier to visualize. We have a list of examples along with step-through visualizations:
- simplePromise - Shows a simple promise example's execution.
- simpleExpress - Shows a simple express app's execution.
- expressMiddleware - Shows express app with middleware being used.
- setInterval - Illustrates a call to setInterval.
- lazilyInitializedPromise - Illustrates an express app with a promise that is created & resolved in one request's context, and "then'd" in other requests' context.
- markdown-example-1 - Shows example 1 from the Async Context Definitions document
- markdown-example-2 - Shows example 2 from the Async Context Definitionsdocument