├── .gitignore ├── README.md ├── demo.js ├── index.js ├── lib └── CartesianTree.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-event-loop-monitor 2 | ======================= 3 | 4 | NodeJS event loop latency monitor 5 | 6 | Installation 7 | ------------ 8 | 9 | ``` 10 | npm install event-loop-monitor --save 11 | ``` 12 | 13 | Usage 14 | ----- 15 | 16 | ```javascript 17 | var monitor = require('event-loop-monitor'); 18 | 19 | // data event will fire every 4 seconds 20 | monitor.on('data', function(latency) { 21 | console.log(latency); // { p50: 1026, p90: 1059, p95: 1076, p99: 1110, p100: 1260 } 22 | }); 23 | 24 | monitor.resume(); // to start measuring 25 | 26 | // later... 27 | monitor.stop(); // to stop measuring 28 | ``` 29 | 30 | What does it mean? 31 | ------------------ 32 | 33 | In example above this means that in last 4 seconds 50% of events is "late" by 1025 microseconds (1.025ms), 90% is late by 1059 microseconds (1.059ms) and so on. 34 | 35 | CHANGELOG 36 | -------- 37 | 38 | ### 0.1.0 39 | 40 | Removed `.unref()` from scanning interval. From now on, module will hold process running, until `.stop()` is called. 41 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | var monitor = require('./'), 2 | loopDone = false, 3 | afterTicks = 0, 4 | loopCount = 0; 5 | 6 | setInterval(function dummy(){ 7 | // just to keep prcess running 8 | }, 100); 9 | 10 | function loop() { 11 | console.log('Starting heavy loop #%d', loopCount); 12 | var s = +new Date(); 13 | while (true) { 14 | if (+new Date() - s > 5000) { 15 | console.log('Heavy loop #%d is done', loopCount); 16 | loopCount++; 17 | if (loopCount < 5) { 18 | setImmediate(loop); 19 | } else { 20 | loopDone = true; 21 | } 22 | break; 23 | } 24 | } 25 | 26 | } 27 | 28 | monitor.on('data', function(latencyData) { 29 | console.log(JSON.stringify(latencyData, null, 2)); 30 | if (loopDone) { 31 | afterTicks++; 32 | if (afterTicks > 3) { 33 | process.exit(); 34 | } 35 | } 36 | }); 37 | 38 | monitor.resume(); 39 | 40 | console.log('Waiting 10 sec and starting a 5 heavy loops'); 41 | setTimeout(function(){ 42 | loop(); 43 | }, 10000); 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Hi-Res Mode 2 | var 3 | time = process.hrtime(), 4 | ticks = [[0, 0]], 5 | interval = 10, 6 | CartesianTree = require('./lib/CartesianTree'), 7 | util = require("util"), 8 | events = require("events"); 9 | 10 | function EventLoopMonitor() { 11 | 12 | this._loopMonitor = null; 13 | this._counter = null; 14 | 15 | events.EventEmitter.call(this); 16 | } 17 | 18 | util.inherits(EventLoopMonitor, events.EventEmitter); 19 | 20 | EventLoopMonitor.prototype.stop = function() { 21 | clearInterval(this._loopMonitor); 22 | clearInterval(this._counter); 23 | }; 24 | 25 | EventLoopMonitor.prototype.resume = function(counterInterval) { 26 | 27 | var self = this; 28 | 29 | if (isNaN(counterInterval)) counterInterval = 4 * 1000; 30 | 31 | this.stop(); 32 | 33 | this._loopMonitor = setInterval(function() { 34 | ticks.push(process.hrtime(time)); 35 | if (ticks.length == 2) { 36 | time = process.hrtime(); 37 | ticks[0] = [ticks[0][0] - ticks[1][0], ticks[0][1] - ticks[1][1]]; 38 | ticks[1] = [0, 0]; 39 | } 40 | }, interval); 41 | 42 | this._counter = setInterval(function() { 43 | 44 | var _ticks = ticks.reduce(function(obj, val, i){ 45 | if (i > 0) { 46 | var key = Math.floor(((ticks[i][0] - ticks[i - 1][0]) * 1e9 + (ticks[i][1] - ticks[i - 1][1]) - interval * 1e6)/1e3); 47 | obj[key] = obj[key] || 0; 48 | obj[key]++; 49 | } 50 | return obj; 51 | }, {}); 52 | 53 | var ct = new CartesianTree(); 54 | for(var key in _ticks) { 55 | ct.add(parseInt(key, 10), _ticks[key]); 56 | } 57 | 58 | var json = ct.statByPercentile([0.5, 0.9, 0.95, 0.99, 1]); 59 | 60 | self.emit('data', { 61 | 'p50' : Math.floor(json[0.5].k || 0), 62 | 'p90' : Math.floor(json[0.9].k || 0), 63 | 'p95' : Math.floor(json[0.95].k || 0), 64 | 'p99' : Math.floor(json[0.99].k || 0), 65 | 'p100' : Math.floor(json[1].k || 0) 66 | }); 67 | 68 | // https://www.scirra.com/blog/76/how-to-write-low-garbage-real-time-javascript 69 | /* 70 | * Assigning [] to an array is often used as a shorthand to clear it (e.g. 71 | * arr = [];), but note this creates a new empty array and garbages the old 72 | * one! It's better to write arr.length = 0; which has the same effect but 73 | * while re-using the same array object. 74 | */ 75 | ticks[0] = ticks.pop(); 76 | ticks.length = 1; 77 | }, counterInterval); 78 | 79 | }; 80 | 81 | module.exports = new EventLoopMonitor(); 82 | 83 | 84 | -------------------------------------------------------------------------------- /lib/CartesianTree.js: -------------------------------------------------------------------------------- 1 | function mkPatchProperty(proto) { 2 | Object.defineProperty(proto, "patch", { 3 | enumerable : false, 4 | value : function(src) { 5 | var self = this; 6 | Object.getOwnPropertyNames(src).forEach(function(name) { 7 | Object.defineProperty(self, name, Object.getOwnPropertyDescriptor(src, name)); 8 | }); 9 | return this; 10 | } 11 | }); 12 | } 13 | 14 | 15 | /* 16 | - АВЛ-дерево, сбалансированное дерево 17 | http://ru.wikipedia.org/wiki/%D0%90%D0%92%D0%9B-%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE 18 | http://en.wikipedia.org/wiki/AVL_tree 19 | 20 | - Декартово дерево, дерево с приоритетами, Treap, Cartesian Tree 21 | http://habrahabr.ru/post/101818/ 22 | http://ru.wikipedia.org/wiki/%C4%E5%EA%E0%F0%F2%EE%E2%EE_%E4%E5%F0%E5%E2%EE 23 | http://en.wikipedia.org/wiki/Treap 24 | */ 25 | module.exports = CartesianTree; 26 | 27 | function CartesianTree() { 28 | this._root = null; 29 | } 30 | 31 | var p = CartesianTree.prototype; 32 | 33 | /* 34 | * Увеличение веса элемента 35 | */ 36 | p.add = function(key, val) { 37 | var node = this._insert(key); // - вставляем/находим ключ 38 | node.v += val; // - увеличиваем 39 | this._bubbleUp(node); // - "всплываем" элемент 40 | }; 41 | 42 | /* 43 | * Инкремент веса элемента 44 | */ 45 | p.inc = function(key) { 46 | return this.add(key, 1); 47 | }; 48 | 49 | /* 50 | * Уменьшение веса элемента 51 | */ 52 | p.sub = function(key, val) { 53 | var node = this._insert(key); // - вставляем/находим ключ 54 | node.v -= val; // - уменьшаем 55 | this._dropDown(node); // - "проваливаем" элемент 56 | }; 57 | 58 | /* 59 | * Декремент веса элемента 60 | */ 61 | p.dec = function(key) { 62 | return this.sub(key, 1); 63 | }; 64 | 65 | /* 66 | * Обход дерева для редукции 67 | */ 68 | p._reduce = function(node, accumulator, idx, initialValue) { 69 | var res = initialValue; 70 | 71 | if (!node) { 72 | return res; 73 | } 74 | 75 | node.l ? res = this._reduce(node.l, accumulator, idx, res) : 0; 76 | res = accumulator.call( 77 | undefined 78 | , res 79 | , { 80 | key : node.k 81 | , val : node.v 82 | } 83 | , idx + (node.l ? node.l.s : 0) 84 | , this); 85 | node.r ? res = this._reduce(node.r, accumulator, idx + (node.l ? node.l.s : 0) + 1, res) : 0; 86 | return res; 87 | }; 88 | 89 | /* 90 | * Редукция дерева 91 | * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/Reduce 92 | * - prev 93 | * - curr = {key, val} 94 | * - index 95 | * - this 96 | */ 97 | p.reduce = function(accumulator, initialValue) { 98 | if(typeof accumulator !== "function") { 99 | throw new TypeError("First argument is not callable"); 100 | } 101 | return this._reduce(this._root, accumulator, 0, initialValue); 102 | }; 103 | 104 | /* 105 | * Превращение дерева в массив с сортировкой по ключам 106 | */ 107 | p.toArray = function() { 108 | return this.reduce(function(arr, node) { 109 | arr.push(node); 110 | return arr; 111 | }, []); 112 | }; 113 | 114 | /* 115 | * Превращение дерева в объект 116 | */ 117 | p.toObject = function() { 118 | return this.reduce(function(obj, node) { 119 | obj[node.key] = node.val; 120 | return obj; 121 | }, {}); 122 | }; 123 | 124 | 125 | /* 126 | * Рекурсивный поиск значения в массиве 127 | * - включая/исключая само значение 128 | */ 129 | var find = function(args, l, r, val, inclusive) { 130 | if (l >= r - 1) { 131 | return inclusive ? l : r; 132 | } 133 | 134 | var i = l + Math.floor((r - l)/2) 135 | , fl = args[i] < val || (inclusive && args[i] == val); 136 | return find( 137 | args 138 | , fl ? i : l 139 | , fl ? r : i 140 | , val 141 | , inclusive 142 | ); 143 | }; 144 | 145 | var scanByOrder = function(args, rv, l, r, curr) { 146 | if (!curr) { 147 | return; 148 | } 149 | 150 | var wl = l.w + (curr.l ? curr.l.w : 0) 151 | , wr = wl + curr.v 152 | , wt = wr + (curr.r ? curr.r.w : 0); 153 | 154 | /* 155 | * Определение границ текущего узла в массиве порядковых номеров 156 | * - для элемента 2 веса 2 : (1,2) <- (2,2) -> (3,1) 157 | * [ -1, 0, 1 | 2, 3 | 4, 5 ] 158 | * eql| |eqr 159 | */ 160 | 161 | var eq = find(args, l.i, r.i, wl, true) 162 | , eql = eq + (args[eq] < wl ? 1 : 0) 163 | , eqr 164 | , i; 165 | 166 | /* 167 | * Рекурсивный набор для левого потомка и левого диапазона 168 | */ 169 | if (l.i < eql) { 170 | if (curr.l) { 171 | scanByOrder( 172 | args 173 | , rv 174 | , l 175 | , { 176 | i : eql 177 | , s : r.s + (curr.r ? curr.r.s : 0) + 1 178 | , w : r.w + (curr.r ? curr.r.w : 0) + curr.v 179 | } 180 | , curr.l); 181 | } 182 | else { 183 | for (i = l.i; i < eql; i++) { 184 | rv.push({ 185 | k : curr.k 186 | , lt : { 187 | s : l.s 188 | , w : l.w 189 | } 190 | , eq : { 191 | s : (args[i] >= wl && args[i] < wr ? 1 : 0) 192 | , w : (args[i] >= wl && args[i] < wr ? curr.v : 0) 193 | } 194 | , gt : { 195 | s : r.s + (args[i] > wr ? 1 : 0) 196 | , w : r.w + (args[i] > wr ? curr.v : 0) 197 | } 198 | }); 199 | } 200 | } 201 | } 202 | 203 | /* 204 | * Вывод статистик для текущего узла 205 | */ 206 | for (eqr = eql; args[eqr] < wr && eqr < r.i; eqr++) { 207 | if (args[eqr] >= wl) { 208 | rv.push({ 209 | k : curr.k 210 | , lt : { 211 | s : l.s + (curr.l ? curr.l.s : 0) 212 | , w : l.w + (curr.l ? curr.l.w : 0) 213 | } 214 | , eq : { 215 | s : 1 216 | , w : curr.v 217 | } 218 | , gt : { 219 | s : r.s + (curr.r ? curr.r.s : 0) 220 | , w : r.w + (curr.r ? curr.r.w : 0) 221 | } 222 | }); 223 | } 224 | } 225 | 226 | /* 227 | * Рекурсивный набор для правого потомка и правого диапазона 228 | */ 229 | if (eqr < r.i) { 230 | if (curr.r) { 231 | scanByOrder( 232 | args 233 | , rv 234 | , { 235 | i : eqr 236 | , s : l.s + (curr.l ? curr.l.s : 0) + 1 237 | , w : l.w + (curr.l ? curr.l.w : 0) + curr.v 238 | } 239 | , r 240 | , curr.r); 241 | } 242 | else { 243 | for (i = eqr; i < r.i; i++) { 244 | rv.push({ 245 | k : curr.k 246 | , lt : { 247 | s : l.s + (args[i] < wl ? 1 : 0) 248 | , w : l.w + (args[i] < wl ? curr.v : 0) 249 | } 250 | , eq : { 251 | s : (args[i] >= wl && args[i] < wr ? 1 : 0) 252 | , w : (args[i] >= wl && args[i] < wr ? curr.v : 0) 253 | } 254 | , gt : { 255 | s : r.s 256 | , w : r.w 257 | } 258 | }); 259 | } 260 | } 261 | } 262 | }; 263 | 264 | var cmp = function(a, b) {return a - b;}; 265 | 266 | /* 267 | * Class : CartesianTree.StatByOrder 268 | */ 269 | CartesianTree.prototype.StatByOrder = function(self) { 270 | this.patch(self); 271 | }; 272 | 273 | mkPatchProperty(CartesianTree.prototype.StatByOrder.prototype); 274 | 275 | /* 276 | * Множественный набор статистики по порядковым номерам 277 | */ 278 | p.statByOrder = function(args) { 279 | if (args === undefined || args === null) { 280 | return { 281 | s : this._root ? this._root.s : 0 282 | , w : this._root ? this._root.w : 0 283 | }; 284 | } 285 | 286 | var rv = [] 287 | , isArray = args instanceof Array; 288 | 289 | if (isArray && args.length === 0) { 290 | return []; 291 | } 292 | 293 | /* 294 | * 2 -> [ 2 ] 295 | * [ 2, 1, 5, -1, 0, 4, 3 ] -> [ -1, 0, 1, 2, 3, 4, 5 ] 296 | */ 297 | isArray ? args.sort(cmp) : args = [args]; 298 | 299 | /* 300 | * Групповой рекурсивный набор данных 301 | */ 302 | scanByOrder( 303 | args 304 | , rv 305 | , { 306 | i : 0 307 | , s : 0 308 | , w : 0 309 | } 310 | , { 311 | i : args.length 312 | , s : 0 313 | , w : 0 314 | } 315 | , this._root); 316 | 317 | if (rv.length === 0) { 318 | rv.push({ 319 | k : null 320 | , lt : { 321 | s : 0 322 | , w : 0 323 | } 324 | , eq : { 325 | s : 0 326 | , w : 0 327 | } 328 | , gt : { 329 | s : 0 330 | , w : 0 331 | } 332 | } 333 | ); 334 | } 335 | 336 | return new this.StatByOrder(isArray ? args.reduce(function(obj, key, i) { 337 | obj[key] = rv[i]; 338 | return obj; 339 | }, {}) : rv[0]); 340 | }; 341 | 342 | /* 343 | * Class : CartesianTree.StatByPercentile 344 | */ 345 | CartesianTree.prototype.StatByPercentile = function(self) { 346 | this.patch(self); 347 | }; 348 | 349 | mkPatchProperty(CartesianTree.prototype.StatByPercentile.prototype); 350 | 351 | /* 352 | * Множественный набор статистики по персентилям 353 | */ 354 | p.statByPercentile = function(args) { 355 | if (args === undefined || args === null) { 356 | return null; 357 | } 358 | 359 | var ln = (this._root ? this._root.w : 0) 360 | , isArray = args instanceof Array; 361 | 362 | if (isArray && args.length === 0) { 363 | return []; 364 | } 365 | 366 | !isArray ? args = [args] : 0; 367 | 368 | var stats; 369 | 370 | /* 371 | * Набор порядковых номеров элементов для расчета 372 | */ 373 | stats = args.reduce(function(obj, percentile) { 374 | var k = percentile * ln - 1; 375 | 376 | if (k <= 0) { 377 | obj[0] = null; 378 | } 379 | else if (k >= ln - 1) { 380 | obj[ln - 1] = null; 381 | } 382 | else { 383 | obj[Math.floor(k)] = null; 384 | obj[Math.ceil(k)] = null; 385 | } 386 | return obj; 387 | }, {}); 388 | 389 | /* 390 | * Набор статистик по порядковым номерам 391 | */ 392 | stats = this.statByOrder(Object.keys(stats).map(function(val) { 393 | return parseInt(val, 10); 394 | })); 395 | 396 | /* 397 | * Расчет персентилей 398 | */ 399 | stats = args.reduce(function(obj, percentile) { 400 | var k = percentile * ln - 1 401 | , lte = Math.min(Math.max(Math.floor(k + 1), 0), ln) 402 | , rv = {}; 403 | 404 | if (k <= 0) { 405 | rv = stats[0]; 406 | } 407 | else if (k >= ln - 1) { 408 | rv = stats[ln - 1]; 409 | } 410 | else { 411 | var lower = stats[Math.floor(k)] 412 | , upper = stats[Math.ceil(k)]; 413 | 414 | if (lower.k == upper.k) { 415 | rv = lower; 416 | } 417 | else { 418 | rv = { 419 | k : lower.k + (k - Math.floor(k)) * (upper.k - lower.k) 420 | , lt : { 421 | s : lower.lt.s + lower.eq.s 422 | , w : lower.lt.w + lower.eq.w 423 | } 424 | , eq : { 425 | s : 0 426 | , w : 0 427 | } 428 | , gt : { 429 | s : upper.gt.s + upper.eq.s 430 | , w : upper.gt.w + upper.eq.w 431 | } 432 | } 433 | } 434 | } 435 | 436 | obj[percentile] = rv; 437 | return obj; 438 | }, {}); 439 | 440 | return new this.StatByPercentile(isArray ? stats : stats[args[0]]); 441 | }; 442 | 443 | var scanByValue = function(args, rv, l, r, curr) { 444 | var eq = find(args, l.i, r.i, curr.k, true) 445 | , eql = eq + (args[eq] != curr.k ? 1 : 0) 446 | , eqr = eq + 1 447 | , i; 448 | 449 | if (l.i < eql) { 450 | if (curr.l) { 451 | scanByValue( 452 | args 453 | , rv 454 | , l 455 | , { 456 | i : eql 457 | , s : r.s + (curr.r ? curr.r.s : 0) + 1 458 | , w : r.w + (curr.r ? curr.r.w : 0) + curr.v 459 | } 460 | , curr.l); 461 | } 462 | else { 463 | for (i = l.i; i < eql; i++) { 464 | rv.push({ 465 | lte : { 466 | s : l.s + (args[i] == curr.k ? 1 : 0) 467 | , w : l.w + (args[i] == curr.k ? curr.v : 0) 468 | } 469 | , gt : { 470 | s : r.s + (curr.r ? curr.r.s : 0) + (args[i] < curr.k ? 1 : 0) 471 | , w : r.w + (curr.r ? curr.r.w : 0) + (args[i] < curr.k ? curr.v : 0) 472 | } 473 | }); 474 | } 475 | } 476 | } 477 | 478 | for (i = eql; i < eqr; i++) { 479 | rv.push({ 480 | lte : { 481 | s : l.s + (curr.l ? curr.l.s : 0) + 1 482 | , w : l.w + (curr.l ? curr.l.w : 0) + curr.v 483 | } 484 | , gt : { 485 | s : r.s + (curr.r ? curr.r.s : 0) 486 | , w : r.w + (curr.r ? curr.r.w : 0) 487 | } 488 | }); 489 | } 490 | 491 | if (eqr < r.i) { 492 | if (curr.r) { 493 | scanByValue( 494 | args 495 | , rv 496 | , { 497 | i : eqr 498 | , s : l.s + (curr.l ? curr.l.s : 0) + 1 499 | , w : l.w + (curr.l ? curr.l.w : 0) + curr.v 500 | } 501 | , r 502 | , curr.r); 503 | } 504 | else { 505 | for (i = eqr; i < r.i; i++) { 506 | rv.push({ 507 | lte : { 508 | s : l.s + (curr.l ? curr.l.s : 0) + 1 509 | , w : l.w + (curr.l ? curr.l.w : 0) + curr.v 510 | } 511 | , gt : { 512 | s : r.s 513 | , w : r.w 514 | } 515 | }); 516 | } 517 | } 518 | } 519 | }; 520 | 521 | /* 522 | * Множественный набор статистики по значениям 523 | */ 524 | p.statByValue = function(args) { 525 | if (args === undefined || args === null) { 526 | return null; 527 | } 528 | 529 | var rv = [] 530 | , isArray = args instanceof Array; 531 | 532 | if (isArray && args.length === 0) { 533 | return []; 534 | } 535 | 536 | isArray ? args.sort(cmp) : args = [args]; 537 | 538 | /* 539 | * Убираем дубли 540 | */ 541 | args = args.filter(function(val, i) { 542 | return i === 0 || val != args[i - 1]; 543 | }); 544 | 545 | /* 546 | * Групповой рекурсивный набор данных 547 | */ 548 | scanByValue( 549 | args 550 | , rv 551 | , { 552 | i : 0 553 | , s : 0 554 | , w : 0 555 | } 556 | , { 557 | i : args.length 558 | , s : 0 559 | , w : 0 560 | } 561 | , this._root); 562 | 563 | if (rv.length === 0) { 564 | rv.push({ 565 | lte : { 566 | s : 0 567 | , w : 0 568 | } 569 | , gt : { 570 | s : 0 571 | , w : 0 572 | } 573 | }); 574 | } 575 | 576 | return isArray ? args.reduce(function(obj, key, i) { 577 | obj[key] = rv[i]; 578 | return obj; 579 | }, {}) : rv[0]; 580 | }; 581 | 582 | /* 583 | * Вывод структуры дерева 584 | */ 585 | p.print = function() { 586 | console.log('\n' + require('util').inspect(this._root, showHidden = false, depth = null, colorize = true)); 587 | }; 588 | 589 | // ---- private 590 | 591 | /* 592 | * Поиск элемента 593 | */ 594 | p._find = function(key, node) { 595 | node = node || this._root; 596 | 597 | if (!node) { 598 | return [null, key]; 599 | } 600 | 601 | if (key == node.k) { 602 | return node; 603 | } 604 | 605 | var next = node[key < node.k ? 'l' : 'r']; 606 | return next ? this._find(key, next) : [node, key - node.k]; 607 | }; 608 | 609 | /* 610 | * Class : CartesianTree.Node 611 | */ 612 | CartesianTree.prototype.Node = function(key) { 613 | this.k = key; // узел : ключ 614 | this.v = 0; // узел : значение/вес/приоритет 615 | this.s = 1; // поддерево : размер 616 | this.d = 1; // поддерево : глубина 617 | this.w = 0; // поддерево : вес 618 | 619 | this.p = null; 620 | this.l = null; 621 | this.r = null; 622 | } 623 | 624 | /* 625 | * Вставка (поиск) элемента в дереве 626 | */ 627 | p._insert = function(key) { 628 | var result = this._find(key); 629 | 630 | if (result instanceof Array) { // - вставка 631 | var nodeP = result[0] 632 | , nodeC = new this.Node(key); 633 | 634 | if (nodeP) { // - предок есть 635 | nodeC.p = nodeP; 636 | nodeP[result[1] < 0 ? 'l' : 'r'] = nodeC; 637 | } 638 | else { // - предка нет, текущий узел - корень 639 | this._root = nodeC; 640 | } 641 | return nodeC; 642 | } 643 | return result; // - узел найден 644 | }; 645 | 646 | /* 647 | * Малое вращение дерева 648 | */ 649 | p._rotateSmall = function(node) { 650 | var l0 = node 651 | , l1 = node.p; 652 | 653 | if (!l1.p) { 654 | l0.p = null; 655 | this._root = l0; 656 | } 657 | else { 658 | l0.p = l1.p; 659 | l1.p[l1.k < l1.p.k ? 'l' : 'r'] = l0; 660 | } 661 | 662 | if (l0[l0.k > l1.k ? 'l' : 'r']) { 663 | l0[l0.k > l1.k ? 'l' : 'r'].p = l1; 664 | l1[l0.k < l1.k ? 'l' : 'r'] = l0[l0.k > l1.k ? 'l' : 'r']; 665 | } 666 | else { 667 | l1[l0.k < l1.k ? 'l' : 'r'] = null; 668 | } 669 | 670 | l1.p = l0; 671 | l0[l0.k > l1.k ? 'l' : 'r'] = l1; 672 | 673 | this._updateNode(l1); 674 | this._updateNode(l0); 675 | if (l0.p) { 676 | this._updateNode(l0.p); 677 | } 678 | }; 679 | 680 | /* 681 | * Большое вращение дерева 682 | */ 683 | p._rotateLarge = function(node) { 684 | this._rotateSmall(node); 685 | this._rotateSmall(node); 686 | }; 687 | 688 | /* 689 | * Балансировка дерева 690 | */ 691 | p._balance = function(node) { 692 | var l0 = node 693 | , l1 = l0.p; 694 | 695 | // нет предка - нечего балансировать 696 | if (!l1) { 697 | return false; 698 | } 699 | 700 | var l1dl = l1.l ? l1.l.d : 0 701 | , l1dr = l1.r ? l1.r.d : 0; 702 | 703 | if ( Math.abs(l1dl - l1dr) > 1 // - есть дисбаланс 704 | && l0.v == l1.v // - совпадают веса 705 | && (l1.k < l0.k) == (l1dl < l1dr)) { // - совпадают направления обхода и дисбаланса 706 | this._rotateSmall(l0); // - меняем их местами 707 | return true; 708 | } 709 | 710 | var l2 = l1.p; 711 | 712 | // нет над-предка - нечего балансировать 713 | if (!l2) { 714 | return false; 715 | } 716 | 717 | if ((l0.k < l1.k) == (l1.k < l2.k)) { // - совпадают направления обхода 718 | return this._balance(l1); // - проверяем баланс предка 719 | } 720 | else { 721 | var l2dl = l2.l ? l2.l.d : 0 722 | , l2dr = l2.r ? l2.r.d : 0; 723 | 724 | if ( Math.abs(l2dl - l2dr) > 1 // - есть дисбаланс 725 | && l0.v == l2.v) { // - совпадают веса 726 | this._rotateLarge(l0); // - меняем их местами 727 | return true; 728 | } 729 | } 730 | return false; 731 | }; 732 | 733 | /* 734 | * Обновление статистики поддерева узла 735 | */ 736 | p._updateNode = function(node) { 737 | if (node.l && node.r) { 738 | node.d = Math.max(node.l.d, node.r.d) + 1; 739 | node.s = node.l.s + node.r.s + 1; 740 | node.w = node.l.w + node.r.w + node.v; 741 | } 742 | else if (!node.l && !node.r) { 743 | node.d = 1; 744 | node.s = 1; 745 | node.w = node.v; 746 | } 747 | else if (node.l) { 748 | node.d = node.l.d + 1; 749 | node.s = node.l.s + 1; 750 | node.w = node.l.w + node.v; 751 | } 752 | else { 753 | node.d = node.r.d + 1; 754 | node.s = node.r.s + 1; 755 | node.w = node.r.w + node.v; 756 | } 757 | }; 758 | 759 | /* 760 | * Обновление глубины и размера поддеревьев 761 | */ 762 | p._updateTree = function(node) { 763 | var curr; 764 | for (curr = node; curr; curr = curr.p) { 765 | this._updateNode(curr); 766 | } 767 | for (curr = node; curr; curr = curr.p) { 768 | this._balance(curr); 769 | } 770 | }; 771 | 772 | /* 773 | * "Всплытие" элемента с большим весом 774 | */ 775 | p._bubbleUp = function(node) { 776 | while (node.p && node.p.v < node.v) { 777 | this._rotateSmall(node); 778 | } 779 | this._updateTree(node); 780 | }; 781 | 782 | /* 783 | * "Всплытие" элемента с большим весом 784 | */ 785 | p._deleteNode = function(node) { 786 | while (node.p && node.p.v < node.v) { 787 | this._rotateSmall(node); 788 | } 789 | this._updateTree(node); 790 | }; 791 | 792 | /* 793 | * "Проваливание" элемента с меньшим весом 794 | */ 795 | p._dropDown = function(node, first) { 796 | var l0 = node 797 | , l1 = node.p; 798 | 799 | if (!l0.l && !l0.r && l0.v === 0) { 800 | if (!l1) { 801 | this._root = null; 802 | } 803 | else { 804 | l1[l0.k < l1.k ? 'l' : 'r'] = null; 805 | this._updateNode(l1); 806 | } 807 | 808 | if (first !== false) { 809 | this._updateTree(l1); 810 | } 811 | return; 812 | } 813 | 814 | var lv = (l0.l ? l0.l.v : 0) 815 | , rv = (l0.r ? l0.r.v : 0) 816 | , ld = (l0.l ? l0.l.d : 0) 817 | , rd = (l0.r ? l0.r.d : 0) 818 | , lw = (l0.l ? l0.l.w : 0) 819 | , rw = (l0.r ? l0.r.w : 0); 820 | 821 | if (l0.v < lv || l0.v < rv) { 822 | this._rotateSmall(lv > rv || (lv == rv && ld > rd) || (lv == rv && ld == rd && lw > rw) ? l0.l : l0.r); 823 | this._dropDown(l0, false); 824 | } 825 | 826 | if (first !== false) { 827 | this._updateTree(!l0.l && !l0.r && Math.abs(ld - rd) < 2 ? l0 : l0[ld > rd ? 'l' : 'r']); 828 | } 829 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-loop-monitor", 3 | "version": "0.2.0", 4 | "description": "NodeJS event loop latency monitor", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Olegas/node-event-loop-monitor.git" 12 | }, 13 | "keywords": [ 14 | "eventloop", 15 | "latency" 16 | ], 17 | "author": "Kirill Borovikov ", 18 | "maintainer": "Oleg Elifantiev ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Olegas/node-event-loop-monitor/issues" 22 | } 23 | } 24 | --------------------------------------------------------------------------------