Skip to content

Commit c3b89df

Browse files
Julien Gillimscdex
Julien Gilli
authored andcommitted
timers: Avoid linear scan in _unrefActive.
Before this change, _unrefActive would keep the unrefList sorted when adding a new timer. Because _unrefActive is called extremely frequently, this linear scan (O(n) at worse) would make _unrefActive show high in the list of contributors when profiling CPU usage. This commit changes _unrefActive so that it doesn't try to keep the unrefList sorted. The insertion thus happens in constant time. However, when a timer expires, unrefTimeout has to go through the whole unrefList because it's not ordered anymore. It is usually not large enough to have a significant impact on performance because: - Most of the time, the timers will be removed before unrefTimeout is called because their users (sockets mainly) cancel them when an I/O operation takes place. - If they're not, it means that some I/O took a long time to happen, and the initiator of subsequents I/O operations that would add more timers has to wait for them to complete. With this change, _unrefActive does not show as a significant contributor in CPU profiling reports anymore. Fixes nodejs#8160. PR-URL: nodejs#8751 Signed-off-by: Timothy J Fontaine <[email protected]>
1 parent 7d0af3b commit c3b89df

File tree

2 files changed

+144
-47
lines changed

2 files changed

+144
-47
lines changed

lib/timers.js

+73-47
Original file line numberDiff line numberDiff line change
@@ -410,39 +410,89 @@ function unrefTimeout() {
410410

411411
debug('unrefTimer fired');
412412

413-
var first;
414-
while (first = L.peek(unrefList)) {
415-
var diff = now - first._monotonicStartTime;
413+
var timeSinceLastActive;
414+
var nextTimeoutTime;
415+
var nextTimeoutDuration;
416+
var minNextTimeoutTime;
417+
var itemToDelete;
418+
419+
// The actual timer fired and has not yet been rearmed,
420+
// let's consider its next firing time is invalid for now.
421+
// It may be set to a relevant time in the future once
422+
// we scanned through the whole list of timeouts and if
423+
// we find a timeout that needs to expire.
424+
unrefTimer.when = -1;
416425

417-
if (diff < first._idleTimeout) {
418-
diff = first._idleTimeout - diff;
419-
unrefTimer.start(diff, 0);
420-
unrefTimer.when = now + diff;
421-
debug('unrefTimer rescheudling for later');
422-
return;
426+
// Iterate over the list of timeouts,
427+
// call the onTimeout callback for those expired,
428+
// and rearm the actual timer if the next timeout to expire
429+
// will expire before the current actual timer.
430+
var cur = unrefList._idlePrev;
431+
while (cur != unrefList) {
432+
timeSinceLastActive = now - cur._monotonicStartTime;
433+
434+
if (timeSinceLastActive < cur._idleTimeout) {
435+
// This timer hasn't expired yet, but check if its expiring time is
436+
// earlier than the actual timer's expiring time
437+
438+
nextTimeoutDuration = cur._idleTimeout - timeSinceLastActive;
439+
nextTimeoutTime = now + nextTimeoutDuration;
440+
if (minNextTimeoutTime == null ||
441+
(nextTimeoutTime < minNextTimeoutTime)) {
442+
// We found a timeout that will expire earlier,
443+
// store its next timeout time now so that we
444+
// can rearm the actual timer accordingly when
445+
// we scanned through the whole list.
446+
minNextTimeoutTime = nextTimeoutTime;
447+
}
448+
449+
// This timer hasn't expired yet, skipping
450+
cur = cur._idlePrev;
451+
continue;
423452
}
424453

425-
L.remove(first);
454+
// We found a timer that expired
455+
var domain = cur.domain;
426456

427-
var domain = first.domain;
457+
if (!cur._onTimeout) continue;
428458

429-
if (!first._onTimeout) continue;
430-
if (domain && domain._disposed) continue;
459+
if (domain && domain._disposed)
460+
continue;
431461

432462
try {
433-
if (domain) domain.enter();
434463
var threw = true;
464+
465+
if (domain) domain.enter();
466+
467+
itemToDelete = cur;
468+
// Move to the previous item before calling the _onTimeout callback,
469+
// as it can mutate the list.
470+
cur = cur._idlePrev;
471+
472+
// Remove the timeout from the list because it expired.
473+
L.remove(itemToDelete);
474+
435475
debug('unreftimer firing timeout');
436-
first._onTimeout();
476+
itemToDelete._onTimeout();
477+
437478
threw = false;
438-
if (domain) domain.exit();
479+
480+
if (domain)
481+
domain.exit();
439482
} finally {
440483
if (threw) process.nextTick(unrefTimeout);
441484
}
442485
}
443486

444-
debug('unrefList is empty');
445-
unrefTimer.when = -1;
487+
// Rearm the actual timer with the timeout delay
488+
// of the earliest timeout found.
489+
if (minNextTimeoutTime != null) {
490+
unrefTimer.start(minNextTimeoutTime - now, 0);
491+
unrefTimer.when = minNextTimeoutTime;
492+
debug('unrefTimer rescheduled');
493+
} else if (L.isEmpty(unrefList)) {
494+
debug('unrefList is empty');
495+
}
446496
}
447497

448498

@@ -471,38 +521,14 @@ exports._unrefActive = function(item) {
471521
item._idleStart = nowDate;
472522
item._monotonicStartTime = nowMonotonicTimestamp;
473523

474-
if (L.isEmpty(unrefList)) {
475-
debug('unrefList empty');
476-
L.append(unrefList, item);
524+
var when = nowMonotonicTimestamp + msecs;
477525

526+
// If the actual timer is set to fire too late, or not set to fire at all,
527+
// we need to make it fire earlier
528+
if (unrefTimer.when === -1 || unrefTimer.when > when) {
478529
unrefTimer.start(msecs, 0);
479-
unrefTimer.when = nowMonotonicTimestamp + msecs;
530+
unrefTimer.when = when;
480531
debug('unrefTimer scheduled');
481-
return;
482-
}
483-
484-
var when = nowMonotonicTimestamp + msecs;
485-
486-
debug('unrefList find where we can insert');
487-
488-
var cur, them;
489-
490-
for (cur = unrefList._idlePrev; cur != unrefList; cur = cur._idlePrev) {
491-
them = cur._monotonicStartTime + cur._idleTimeout;
492-
493-
if (when < them) {
494-
debug('unrefList inserting into middle of list');
495-
496-
L.append(cur, item);
497-
498-
if (unrefTimer.when > when) {
499-
debug('unrefTimer is scheduled to fire too late, reschedule');
500-
unrefTimer.start(msecs, 0);
501-
unrefTimer.when = when;
502-
}
503-
504-
return;
505-
}
506532
}
507533

508534
debug('unrefList append to end');
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright Joyent, Inc. and other Node contributors.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a
4+
// copy of this software and associated documentation files (the
5+
// "Software"), to deal in the Software without restriction, including
6+
// without limitation the rights to use, copy, modify, merge, publish,
7+
// distribute, sublicense, and/or sell copies of the Software, and to permit
8+
// persons to whom the Software is furnished to do so, subject to the
9+
// following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included
12+
// in all copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17+
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20+
// USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
/*
23+
* This test is aimed at making sure that unref timers queued with
24+
* timers._unrefActive work correctly.
25+
*
26+
* Basically, it queues one timer in the unref queue, and then queues
27+
* it again each time its timeout callback is fired until the callback
28+
* has been called ten times.
29+
*
30+
* At that point, it unenrolls the unref timer so that its timeout callback
31+
* is not fired ever again.
32+
*
33+
* Finally, a ref timeout is used with a delay large enough to make sure that
34+
* all 10 timeouts had the time to expire.
35+
*/
36+
37+
var timers = require('timers');
38+
var assert = require('assert');
39+
40+
var someObject = {};
41+
var nbTimeouts = 0;
42+
43+
/*
44+
* libuv 0.10.x uses GetTickCount on Windows to implement timers, which uses
45+
* system's timers whose resolution is between 10 and 16ms. See
46+
* http://msdn.microsoft.com/en-us/library/windows/desktop/ms724408.aspx
47+
* for more information. That's the lowest resolution for timers across all
48+
* supported platforms. We're using it as the lowest common denominator,
49+
* and thus expect 5 timers to be able to fire in under 100 ms.
50+
*/
51+
var N = 5;
52+
var TEST_DURATION = 100;
53+
54+
timers.unenroll(someObject);
55+
timers.enroll(someObject, 1);
56+
57+
someObject._onTimeout = function _onTimeout() {
58+
++nbTimeouts;
59+
60+
if (nbTimeouts === N) timers.unenroll(someObject);
61+
62+
timers._unrefActive(someObject);
63+
}
64+
65+
function startTimer() { timers._unrefActive(someObject); }
66+
67+
startTimer();
68+
69+
setTimeout(function() {
70+
assert.equal(nbTimeouts, N);
71+
}, TEST_DURATION);

0 commit comments

Comments
 (0)