├── .npmignore ├── .gitignore ├── src ├── eventLoopStats.js ├── eventLoopStats.d.ts └── eventLoopStats.cc ├── .editorconfig ├── binding.gyp ├── test ├── types.ts └── eventLoopStats.js ├── CHANGELOG.md ├── example.js ├── .github └── workflows │ └── verify.yml ├── package.json ├── README.md └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /src/eventLoopStats.js: -------------------------------------------------------------------------------- 1 | var eventLoopStats = require('../build/Release/eventLoopStats'); 2 | exports.sense = eventLoopStats.sense; 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [{ 3 | "target_name" : "eventLoopStats", 4 | "sources" : [ "src/eventLoopStats.cc" ], 5 | "include_dirs" : [ 6 | "src", 7 | "", 12 | "license": "MIT", 13 | "keywords": [ 14 | "libuv", 15 | "stats", 16 | "monitoring", 17 | "loop" 18 | ], 19 | "scripts": { 20 | "install": "node-gyp rebuild", 21 | "rebuild": "node-gyp rebuild", 22 | "test": "mocha && tsc --noEmit --strict --esModuleInterop --module commonjs test/types.ts" 23 | }, 24 | "engines": { 25 | "node": ">=4.0.0" 26 | }, 27 | "dependencies": { 28 | "nan": "^2.14.0" 29 | }, 30 | "gypfile": true, 31 | "devDependencies": { 32 | "@types/node": "^17.0.10", 33 | "chai": "4.1.2", 34 | "mocha": "10.2.0", 35 | "typescript": "^4.5.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

event-loop-stats

2 |

Exposes stats about the libuv default loop

3 | 4 | ## Installation 5 | 6 | ``` 7 | npm install --save event-loop-stats 8 | ``` 9 | 10 | ## Usage 11 | ```javascript 12 | var eventLoopStats = require('event-loop-stats'); 13 | console.log('Stats', eventLoopStats.sense()); 14 | ``` 15 | 16 | This will print the following information: 17 | 18 | ``` 19 | Stats { 20 | max: 5, 21 | min: 0, 22 | sum: 10, 23 | num: 5 24 | } 25 | ``` 26 | 27 | ## Property insights 28 | - `max`: Maximum number of milliseconds spent in a single loop since last `sense call`. 29 | - `min`: Minimum number of milliseconds spent in a single loop since last `sense call`. 30 | - `sum`: Total number of milliseconds spent in the loop since last `sense call`. 31 | - `num`: Total number of loops since last `sense call`. 32 | 33 | ## Node version compatibility 34 | `event-loop-stats` depends on C++ extensions which are compiled when the `event-loop-stats` module is installed. Compatibility information can be inspected via the [Travis-CI build jobs](https://travis-ci.org/bripkens/event-loop-stats). 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2022 Ben Blackmore and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/eventLoopStats.js: -------------------------------------------------------------------------------- 1 | var eventLoopStats = require('..'); 2 | var expect = require('chai').expect; 3 | 4 | describe('eventLoopStats', function() { 5 | it('should expose a sense function', function() { 6 | expect(eventLoopStats.sense).to.be.a('function'); 7 | }); 8 | 9 | it('should return stats via the sense function', function(done) { 10 | setTimeout(function() { 11 | // provoke execution on the default loop 12 | }, 500); 13 | 14 | setTimeout(function() { 15 | var stats = eventLoopStats.sense(); 16 | expect(stats.num).to.be.a('number'); 17 | expect(stats.num).to.be.gt(0); 18 | expect(stats.sum).to.be.a('number'); 19 | expect(stats.max).to.be.a('number'); 20 | expect(stats.min).to.be.a('number'); 21 | done(); 22 | }, 1000); 23 | }); 24 | 25 | // This pattern will usually allow for at least _two_ on_check calls between 26 | // the two sense calls. Check the next test for a slightly different pattern. 27 | it('should detect a blocked event loop', function(done) { 28 | // Call sense to reset max. 29 | eventLoopStats.sense(); 30 | // On the next tick, block for 500ms. 31 | setTimeout(function() { 32 | var waitUntill = new Date(Date.now() + 500); 33 | // Block event loop with busy waiting. 34 | while (waitUntill > new Date()) {} 35 | 36 | // On the next tick, detect stats again - the 500 ms block should have 37 | // been noticed. 38 | setTimeout(function() { 39 | var stats = eventLoopStats.sense(); 40 | expect(stats.max).to.be.gte(490); 41 | expect(stats.max).to.be.lt(2000); 42 | expect(stats.sum).to.be.gte(490); 43 | expect(stats.sum).to.be.lt(2000); 44 | 45 | // At least two on_check calls should have happened (older Node.js versions will call it more often). 46 | expect(stats.num).to.be.gte(2); 47 | 48 | // Since there are at least two on_check calls, min and max should be different. 49 | expect(stats.min).to.be.gte(0); 50 | expect(stats.min).to.be.lt(stats.max); 51 | 52 | done(); 53 | }, 0); 54 | }, 0); 55 | }); 56 | 57 | // This pattern will typically allow for only _one_ on_check call between the 58 | // two sense calls (at least on more recent Node.js versions). Check the 59 | // previous test for a slightly different pattern. 60 | it('one on_check call should suffice to report correct max duration ', function(done) { 61 | // Call sense to reset max. 62 | eventLoopStats.sense(); 63 | var now = Date.now(); 64 | var end = now + 50; 65 | setTimeout(function() { 66 | // After 100ms, call sense again - the 50ms block (see below) should have been noticed. 67 | var stats = eventLoopStats.sense(); 68 | expect(stats.max).to.be.gte(40); 69 | expect(stats.max).to.be.lt(1000); 70 | expect(stats.sum).to.be.gte(40); 71 | expect(stats.sum).to.be.lt(1000); 72 | 73 | // Only one on_check call might have happened. 74 | expect(stats.num).to.gte(1); 75 | 76 | done(); 77 | }, 100); 78 | 79 | // Directly block for 50ms right in this tick, synchronously. 80 | while (Date.now() < end) {} 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/eventLoopStats.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace v8; 4 | 5 | // Casting -1 to an uint will give the max uint value 6 | const uint32_t maxPossibleUint32 = -1; 7 | const uint64_t maxPossibleUint64 = -1; 8 | const uint32_t minPossibleUint32 = 0; 9 | 10 | uv_check_t check_handle; 11 | 12 | // The minimum event loop duration since the last sense() call. 13 | uint32_t min; 14 | // The maximum event loop duration since the last sense() call. 15 | uint32_t max; 16 | // The sum of event loop durations the last sense() call. 17 | uint32_t sum; 18 | // The number of event loop iterations since the last sense() call. 19 | uint32_t num; 20 | 21 | uint64_t previous_now = maxPossibleUint64; 22 | 23 | // This will be called after each sense call. 24 | void reset() { 25 | min = maxPossibleUint32; 26 | max = minPossibleUint32; 27 | sum = 0; 28 | num = 0; 29 | } 30 | 31 | // This will be called once per event loop iteration, right after polling 32 | // for i/o. See http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop for a 33 | // general overview and http://docs.libuv.org/en/v1.x/check.html for details. 34 | void on_check(uv_check_t* handle) { 35 | // An earlier incarnation of event-loop-stats checked uv_hrtime() against 36 | // uv_now(handle->loop), which doesn't seem to work -- for example, it does 37 | // not detect long event loop iterations caused by a synchronous block due to 38 | // long running for- or while-loops. Checking against the time of the same 39 | // point at the last iteration of the event loop also covers these cases. 40 | 41 | const uint64_t now = uv_hrtime(); 42 | uint32_t duration; 43 | 44 | if (previous_now >= now) { 45 | // This only happens on the very first call on_check call. Since we have no 46 | // timestamp to compare to from an earlier on_check call, we start by 47 | // assuming an event loop lag of zero. 48 | duration = 0; 49 | } else { 50 | // Calculate the duration since the last on_check call - this is the 51 | // event loop lag. 52 | // And convert to milliseconds (uv_hrtime yields nanos). 53 | duration = (now - previous_now) / static_cast(1e6); 54 | } 55 | 56 | // save min/max values 57 | if (duration < min) { 58 | min = duration; 59 | } 60 | if (duration > max) { 61 | max = duration; 62 | } 63 | 64 | // Sum up all durations between two consecutive sense() calls. 65 | sum += duration; 66 | 67 | // Simply count all event loop iterations between two sense() calls. 68 | num += 1; 69 | 70 | // Save the current timestamp for the next on_check call for comparison. 71 | previous_now = now; 72 | } 73 | 74 | 75 | static NAN_METHOD(sense) { 76 | // Reset min and max counters when there were no calls. 77 | if (num == 0) { 78 | min = 0; 79 | max = 0; 80 | } 81 | 82 | Local obj = Nan::New(); 83 | Nan::Set( 84 | obj, 85 | Nan::New("min").ToLocalChecked(), 86 | Nan::New(static_cast(min)) 87 | ); 88 | Nan::Set( 89 | obj, 90 | Nan::New("max").ToLocalChecked(), 91 | Nan::New(static_cast(max)) 92 | ); 93 | Nan::Set( 94 | obj, 95 | Nan::New("num").ToLocalChecked(), 96 | Nan::New(static_cast(num)) 97 | ); 98 | Nan::Set( 99 | obj, 100 | Nan::New("sum").ToLocalChecked(), 101 | Nan::New(static_cast(sum)) 102 | ); 103 | 104 | reset(); 105 | 106 | info.GetReturnValue().Set(obj); 107 | } 108 | 109 | 110 | NAN_MODULE_INIT(init) { 111 | reset(); 112 | 113 | uv_check_init(uv_default_loop(), &check_handle); 114 | uv_check_start(&check_handle, reinterpret_cast(on_check)); 115 | uv_unref(reinterpret_cast(&check_handle)); 116 | 117 | Nan::Set(target, 118 | Nan::New("sense").ToLocalChecked(), 119 | Nan::GetFunction(Nan::New(sense)).ToLocalChecked() 120 | ); 121 | } 122 | 123 | NODE_MODULE(eventLoopStats, init) 124 | --------------------------------------------------------------------------------