1 | # Comparing Node.js Asynchronous Alternatives
|
2 |
|
3 |
|
4 |
|
5 | ### The Sample Functions
|
6 | The `largest`, `countFiles`, and `fibonacci` folders each contain a sample function implemented in six different ways:
|
7 |
|
8 | * **async**: using the [`async`](https://github.com/caolan/async) library.
|
9 | * **asyncawait**: using this `asyncawait` library.
|
10 | * **bluebird**: using the [`bluebird`](https://github.com/petkaantonov/bluebird) library.
|
11 | * **callbacks**: using plain callbacks.
|
12 | * **co**: using the [`co`](https://github.com/visionmedia/co) library (requires node >= 0.11.2 with the `--harmony` flag).
|
13 | * **synchronous**: using plain blocking code (just for comparison).
|
14 |
|
15 | This gives a good indication of the trade-offs between the different coding styles. For the remainer of this document, we'll focus on the most complex sample function, the `largest()` function.
|
16 |
|
17 |
|
18 |
|
19 | ### The `largest()` Function
|
20 | The `largest()` sample function is designed to be of moderate complexity, like a real-world problem.
|
21 |
|
22 | `largest(dir, options)` finds the largest file in the given directory, optionally performing a recursive search. `dir` is the path of the directory to search. `options`, if provided, is a hash with the following two keys, both optional:
|
23 |
|
24 | * `recurse` (`boolean`, defaults to `false`): if true, `largest()` will recursively search all subdirectories.
|
25 | * `preview` (`boolean`, defaults to `false`): if true, `largest()` will include a small preview of the largest file's content in it's results.
|
26 |
|
27 | The requirements of `largest()` may be summarised as:
|
28 |
|
29 | 1. Find the largest file in the given directory (recursively searching subdirectories if the option is selected).
|
30 | 2. Keep track of how many files/directories have been processed.
|
31 | 3. Get a preview of the file contents (first several characters) if the option is selected.
|
32 | 4. Return the details about the largest file and the number of files/directories searched.
|
33 | 5. Exploit concurrency wherever possible.
|
34 | 6. Don't block Node's event loop anywhere.
|
35 |
|
36 | The last two requirements are obviously violated by the 'synchronous' variant, but it is worth including for comparison.
|
37 |
|
38 |
|
39 |
|
40 | ### Metrics for Comparison
|
41 | Some interesting metrics with which to compare the six variants are:
|
42 |
|
43 | * **Lines of code (SLOC)**: Shorter code that does the same thing is usually a good thing.
|
44 | * **Levels of Indenting**: Each indent represents a context-shift and therefore higher complexity.
|
45 | * **Anachrony**: Asynchronous code may execute in an order very different from its visual representation, which may make it harder to read and reason about in some cases.
|
46 | * **Speed**: Node.js is built for speed and throughput, so any loss of speed imposed by a variant may count against it
|
47 |
|
48 |
|
49 | # Comparison Summary
|
50 | The following metrics are for the `largest()` example function:
|
51 |
|
52 | | Variant | SLOC <sup>[1]</sup> | Indents <sup>[2]</sup> | Anachrony <sup>[3]</sup> | Ops/sec <sup>[4]</sup> |
|
53 | | :------------ | -------: | ----------: | ------------: | ----------: |
|
54 | | async | 67 | 7 | 5 | ~65 |
|
55 | | asyncawait | 23 | 2 | - | ~79 |
|
56 | | bluebird | 44 | 3 | 8 | ~89 |
|
57 | | callbacks | 84 | 6 | 9 | ~100 |
|
58 | | co | 23 | 2 | - | ~68 <sup>[5]</sup> |
|
59 | | synchronous | 23 | 2 | - | ~63 <sup>[6]</sup> |
|
60 |
|
61 | ###### Footnotes:
|
62 |
|
63 | <sup>[1]</sup> Includes only lines in the function body; excludes blank lines and comment lines.
|
64 |
|
65 | <sup>[2]</sup> Maximum indentation from the outermost statements in the function body.
|
66 |
|
67 | <sup>[3]</sup> Count of times in the function body when visually lower statements execute before visually higher statements due to asynchronous callbacks.
|
68 |
|
69 | <sup>[4]</sup> Scaled (callbacks = 100), higher is better. Using [benchmark.js](./benchmark.js) on my laptop. All benchmarks run in Node v0.10.25 except for `co` - see [5] below.
|
70 |
|
71 | <sup>[5]</sup> `co` benchmark run in Node v0.11.12 with the `--harmony` flag.
|
72 |
|
73 | <sup>[6]</sup> Not strictly comparable because it blocks Node's event loop.
|
74 |
|
75 |
|
76 |
|
77 | # Observations
|
78 | The following observations are based on the above results and obviously may differ substantially with other code and/or on other machines. **YMMV**. Having said that, at least in this case:
|
79 |
|
80 | * Plain callbacks are the speed king.
|
81 | * All other asynchronous variants achieve at least 65% of the speed of plain callbacks.
|
82 | * Bluebird achieves almost 90% of plain callback speed, living up to its reputation of being extremely well optimised.
|
83 | * `asyncawait` is third-fastest in this benchmark, achieving almost 80% of the performance of plain callbacks.
|
84 | * The source code of `co`, `asyncawait`, and `synchronous` are virtually identical, with purely mechanical syntax differences.
|
85 | * `co` and `asyncawait`, each using different coroutine technology, are very similar on these metrics. In a choice between these two, the biggest deciding factor may be whether you can use ES6.
|
86 | * The synchronous approach is actually the slowest, which perhaps makes sense since it can't exploit concurrency.
|
87 | * `async` looks relatively unfavourable compared to the other asynchronous options on these metrics.
|