joyent/node — pull request #3109 by isaacs (2012-04-14 04:11:47+00:00)
Title: Domains3

Submitted for your review.

See the included tests and docs for examples of usage. Most of the time, the user doesn't have to do anything special to take advantage of them: all callbacks that are assigned as part of a `ReqWrap` or timer are automatically attached, as is anything called by `EventEmitter.prototype.emit`.

To enable domains, the domain module must be loaded at least once. `require('domain')` sets the flag on the events module to tell it to register new EventEmitters with the active domain. All the other behavior is based on the presence of an active domain at `process.domain`.

A domain is made "active" by being entered. If a timer or EventEmitter is explicitly added to a domain (or implicitly added by being created while a domain is active), then that domain will be activated for the callback resulting from that timer or EventEmitter. Same for ReqWraps, however since they are not exposed to the user, there is no way to explicitly add them; they can only be implicitly bound to a domain.

The implementation works by hooking into the `process.on('uncaughtException')` event. This has the advantage of catching situations that may escape the current call stack, so is a bit more reliable than wrapping callbacks in a try/catch. However, it still depends on using `MakeCallback` as the singe entry point from C++ to JavaScript, where we use a TryCatch in C++ to route errors to the FatalException exit point.

## Performance

There is a slight performance increase owing to the MakeCallback refactoring, but it's not very significant. The cleanup I did was rather naive, and further performance benefits can perhaps be attained by a more thorough cleanup.

Running a http_simple benchmark against /bytes/1024 shows that they're in roughly the same ballpark. These are pretty typical results, and no significant degradation can be detected.

Note that the http_simple.js benchmark in this branch actually creates a domain and adds both the request and response objects to it explicitly, so it is a meaningful real-world use of domains. Of course, if errors are encountered, then this causes a bit of a performance hit, but otherwise, any impact is less than ambient noise.

On master (that is, 93cefab1a3a40a5693217c9ce43380a1e1cfad4e)

```
$ i=0; while [ $i -lt 10 ] ; do echo -n "$i "; TYPE=bytes LENGTH=1024 NODE_USE_DOMAINS=1 bash benchmark/http.sh 2>&1 | grep Requests ; let "i++" ; done
0 Requests per second: 3403.72 [#/sec] (mean)
1 Requests per second: 2886.88 [#/sec] (mean)
2 Requests per second: 2879.11 [#/sec] (mean)
3 Requests per second: 2918.01 [#/sec] (mean)
4 Requests per second: 3053.11 [#/sec] (mean)
5 Requests per second: 2821.80 [#/sec] (mean)
6 Requests per second: 2883.54 [#/sec] (mean)
7 Requests per second: 2734.61 [#/sec] (mean)
8 Requests per second: 2914.67 [#/sec] (mean)
9 Requests per second: 2813.31 [#/sec] (mean)
```

After the MakeCallback refactoring, but before the actual domain work:

```
[gh:node/pre-domain]isaacs@tum.local:~/dev/js/node-master v0.7.8-pre 4.1.0(1)-release
$ i=0; while [ $i -lt 10 ] ; do echo -n "$i "; TYPE=bytes LENGTH=1024 NODE_USE_DOMAINS=1 bash benchmark/http.sh 2>&1 | grep Requests ; let "i++" ; done
0 Requests per second: 3957.63 [#/sec] (mean)
1 Requests per second: 3145.52 [#/sec] (mean)
2 Requests per second: 3086.34 [#/sec] (mean)
3 Requests per second: 2987.89 [#/sec] (mean)
4 Requests per second: 2718.45 [#/sec] (mean)
5 Requests per second: 3069.57 [#/sec] (mean)
6 Requests per second: 2838.25 [#/sec] (mean)
7 Requests per second: 2851.08 [#/sec] (mean)
8 Requests per second: 2842.76 [#/sec] (mean)
9 Requests per second: 3137.16 [#/sec] (mean)
```

This branch:

```
$ i=0; while [ $i -lt 10 ] ; do echo -n "$i "; TYPE=bytes LENGTH=1024 NODE_USE_DOMAINS=1 bash benchmark/http.sh 2>&1 | grep Requests ; let "i++" ; done
0 Requests per second: 3546.52 [#/sec] (mean)
1 Requests per second: 2911.57 [#/sec] (mean)
2 Requests per second: 2925.34 [#/sec] (mean)
3 Requests per second: 3010.00 [#/sec] (mean)
4 Requests per second: 2982.31 [#/sec] (mean)
5 Requests per second: 2915.03 [#/sec] (mean)
6 Requests per second: 2951.25 [#/sec] (mean)
7 Requests per second: 2912.61 [#/sec] (mean)
8 Requests per second: 2914.82 [#/sec] (mean)
9 Requests per second: 2755.61 [#/sec] (mean)
```

## Shortcomings

* The API feels kind of nice, but it's a bit rough still. I think it's fine for v0.8, and we can refine it further in v0.9 and beyond once users have a chance to play with it. We should not bikeshed it too much at this point. That being said, the method names are sort of weird, and some other things can probably be improved, so if you have any ideas, please share them.

* The `domain.dispose()` mechanism is just kludgey. We really need a single way to forcibly shut down handles, requests, and streams, so that we don't leave timers and fd's dangling. Also on the subject of debuggability, it would be nice to ensure that we don't `close()` a file descriptor while there are still outstanding fs operations on it. This consistency/cleanup can wait, however, and we can make v0.9 all about API polishing.

* The domain only knows about the items that are explicitly added to it. It's too expensive to push every EventEmitter onto the active domain's member list. (It's also extremely memory intensive, though WeakMaps could perhaps be a solution.) So, when you call `domain.dispose()`, it disposes the domain itself (ie, renders it a no-op) but it can only `.destroy()` and `clearTimeout` the items that have been explicitly added by calling `domain.add(emitter)`, not those that are implicitly bound. It would be nice to find some clever way to have the domain cheaply know which items were created during its reign, but I didn't find one.