AsyncWrap is two things. One is a class abstraction that provides an internal mechanism for handling asynchronous tasks, such as calling a callback. The other part is an API for setting up hooks and allows one to get structural tracing information about the life of handle objects. In the context of tracing the latter is usually what is meant.
The reasoning for the current naming confusion is that the API part implements the hooks through the AsyncWrap class, but this is not inherently necessary. For example if v8 provided those facilities the AsyncWrap class would not need be involved in the AsyncWrap API.
For the remaining description the API part is what is meant by AsyncWrap.
AsyncWrap emits events (hooks) that inform the consumer about the life of all handle objects in node. Thus in order to understand AsyncWrap one must first understand handle objects.
Node's core API is mostly defined in JavaScript. However ECMAScript does not define any API for creating a TCP socket, reading a file etc.. That logic is implemented in C++ using libuv and the v8 API. The JavaScript in node core interacts with this C++ layer using the handle objects.
For example in net.connect(port, address) that creates a TCP connection,
two handle objects (TCPConnectWrap and TCP) are created:
const TCP = process.binding('tcp_wrap').TCP;
const TCPConnectWrap = process.binding('tcp_wrap').TCPConnectWrap;
const req = new TCPConnectWrap();
req.oncomplete = oncomplete;
req.address = address;
req.port = port;
const socket = new TCP();
socket.onread = onread;
socket.connect(req, address, port);
// later
socket.destroy();The first handle object (TCPConnectWrap) is for connecting the socket, the
second one (TCP) is for maintaining the connection.
TCPConnectWrap gets its information by setting properties on the handle
object, like address and port. Those properties are read by the C++ layer,
but can also be inspected from the AsyncWrap hooks. When the handle is created
using new TCPConnectWrap() the init hook is called.
An oncomplete property is also set, this is the callback for when the
connection is made or failed. Just before calling oncomplete the pre hook
is called, just after the post hook is called.
The TCP handle works exactly the same way, except that the information
is passed as arguments to a method .connect and the onread function
is called multiple times, thus it behaves like an event. This also means that
the pre and post hooks are called multiple times.
At some later time the socket.destroy() is called, this will call the
destroy hook for the socket handle. Other handle objects aren't explicitly
destroyed, in that case the destroy hook is called when the handle object is
garbage collected by v8.
Thus one should expect the hooks be called in the following order:
init // TCPConnectWrap
init // TCP
=== tick ===
pre // TCPConnectWrap
post // TCPConnectWrap
=== tick ===
pre // TCP
post // TCP
=== tick ===
pre // TCP
post // TCP
=== tick ===
...
=== tick ===
destroy // TCP
=== tick ===
destroy // TCPConnectWraptick indicates there is at least one tick (as in process.nextTick()) between
the text above and the text below.
At the moment there is not a high level API for AsyncWrap. It is simply exposed
through process.binding:
const asyncWrap = process.binding('async_wrap');Be warned that this API is not an official API and can change at any time, even if it's just patch update.
To assign the hooks call:
asyncWrap.setupHooks(init, pre, post, destroy);
function init(provider, uid, parent) { /* consumer code */ }
function pre() { /* consumer code */ }
function post() { /* consumer code */ }
function destroy(uid) { /* consumer code */ }Note that only the init function is required and that calling
asyncWrap.setupHooks again will overwrite the previous hooks.
Because there is a small performance penalty in just calling a noop function, AsyncWrap is not enabled by default. To enable AsyncWrap call:
asyncWrap.enable();Note that handle objects created before AsyncWrap is enabled will never
be tracked, even after .enable() is called. Similarly when AsyncWrap is
disabled, the handle objects there are already being tracked will continue
to be tracked. Finally there are some cases where handle objects have a parent.
For example a TCP server creates TCP sockets, thus the socket handles have the
server handle as a parent. In this case AsyncWrap should be enabled before
the server handle is created, for the TCP sockets to be tracked.
Disabling AsyncWrap can be done with:
asyncWrap.disable();Currently there are 4 hooks: init, pre, post destroy. The this
variable refers to the handle object. The init hook has three extra arguments
provider, uid and parent. The destroy hook also has the uid argument.
function init(provider, uid, parent) { }
function pre() { }
function post() { }
function destroy(uid) { }In the init, pre and post cases the this variable is the handle object.
Users may read properties from this object such as port and address in the
TCPConnectWrap case, or set user specific properties.
In the init hook the handle object is not yet fully constructed, thus some
properties are not safe to read. This causes problems when doing
util.inspect(this) or similar.
In the destroy hook this is null, this is because the handle objects has
been deleted by the garbage collector and thus doesn't exists.
This is an integer that refer to names defined in the asyncWrap.Providers
object map.
At the time of writing this is the current list:
{ NONE: 0,
CRYPTO: 1,
FSEVENTWRAP: 2,
FSREQWRAP: 3,
GETADDRINFOREQWRAP: 4,
GETNAMEINFOREQWRAP: 5,
JSSTREAM: 6,
PIPEWRAP: 7,
PIPECONNECTWRAP: 8,
PROCESSWRAP: 9,
QUERYWRAP: 10,
SHUTDOWNWRAP: 11,
SIGNALWRAP: 12,
STATWATCHER: 13,
TCPWRAP: 14,
TCPCONNECTWRAP: 15,
TIMERWRAP: 16,
TLSWRAP: 17,
TTYWRAP: 18,
UDPWRAP: 19,
UDPSENDWRAP: 20,
WRITEWRAP: 21,
ZLIB: 22 }The uid is a unique integer that identify each handle object. Because the
destroy hook isn't called with the handle object, this is particular useful
for storing information related to the handle object, that the user require in
the destroy hook.
In some cases the handle was created from another handle object. In those
cases the parent argument is set the creating handle object. If there is
no parent handle then it is just null.
The most common case is the TCP server. The TCP server itself is a TCP handle,
but when receiving new connection it creates another TCP handle that is
responsible for the new socket. It does this before emitting the onconnection
handle event, thus the asyncWrap hooks are called in the following order:
init // TCP (socket)
pre // TCP (server)
post // TCP (server)This means it is not possible to know in what handle context the new socket
handle was created using the pre and post hooks. However the
parent argument provides this information.
A classic use case for AsyncWrap is to create a long-stack-trace tool.
const asyncWrap = process.binding('async_wrap');
asyncWrap.setupHooks(init, pre, post);
asyncWrap.enable();
// global state variable, that contains the current stack trace
let currentStack = '';
function init(provider, uid, parent) {
// When a handle is created, collect the stack trace such that we later
// can see what involved the handle constructor.
const localStack = (new Error()).stack.split('\n').slice(1).join('\n');
// Compute the full stack and store it as a property on the handle object,
// such that it can be fetched later.
const extraStack = parent ? parent._full_init_stack : currentStack;
this._full_init_stack = localStack + '\n' + extraStack;
}
function pre() {
// A callback is about to be called, update the `currentStack` such that
// it is correct for when another handle is initialized or `getStack` is
// called.
currentStack = this._full_init_stack;
}
function post() {
// At the time of writing there are some odd cases where there is no handle
// context, this line prevents that from resulting in wrong stack trace. But
// the stack trace will be shorter compared to what ideally should happen.
currentStack = '';
}
function getStack(message) {
const localStack = new Error(message);
return localStack.stack + '\n' + currentStack;
}
module.exports = getStack;Please note that this example is way simpler than what is required from a complete long-stack-trace implementation.
-
It is not obvious when a handle object is created. For example the TCP server creates the
TCPhandle when.listenis called and it may perform an DNS lookup before that. -
console.logis async and thus invokes AsyncWrap, thus usingconsole.loginside one of the hooks, creates an infinite recursion. Usefs.syncWrite(1, msg)orprocess._rawDebug(msg)instead. The latter is a little nicer because it usesutil.inspect. On the other handfs.syncWriteis a documented function. -
process.nextTicknever creates a handle object. You will have to monkey patch this. -
Timer functions (like
setTimeout) shares a singleTimerhandle, thus you will usually have to monkey patch those functions. -
Promises are also not tracked by AsyncWrap. The
Promiseconstructor can also be monkey patched, but unfortunately it is quite difficult. See async-listener for how to do it.
- Status overview of AsyncWrap: #29
- An intro to AsyncWrap by Trevor Norris: http://blog.trevnorris.com/2015/02/asyncwrap-tutorial-introduction.html (outdated)
- Slides from a local talk Andreas Madsen did on AsyncWrap: https://github.com/AndreasMadsen/talk-async-wrap (outdated)
- Complete (hopefully) long-stack-trace module that uses AsyncWrap: https://github.com/AndreasMadsen/trace
- Visualization tool for AsyncWrap: https://github.com/AndreasMadsen/dprof
If you have more info to provide, please send us a pull request!