Skip to content

Commit 6e744c5

Browse files
Julien GilliFishrock123
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/node-v0.x-archive#8160 Signed-off-by: Timothy J Fontaine <[email protected]> Conflicts: lib/timers.js Fixes: nodejs/node-convergence-archive#23 Ref: nodejs#268 PR-URL: nodejs#2540 Reviewed-By: bnoordhuis - Ben Noordhuis <[email protected]>
1 parent d370306 commit 6e744c5

File tree

2 files changed

+123
-47
lines changed

2 files changed

+123
-47
lines changed

lib/timers.js

+72-47
Original file line numberDiff line numberDiff line change
@@ -483,41 +483,90 @@ function unrefTimeout() {
483483

484484
debug('unrefTimer fired');
485485

486-
var diff, domain, first, threw;
487-
while (first = L.peek(unrefList)) {
488-
diff = now - first._idleStart;
486+
var timeSinceLastActive;
487+
var nextTimeoutTime;
488+
var nextTimeoutDuration;
489+
var minNextTimeoutTime;
490+
var itemToDelete;
491+
492+
// The actual timer fired and has not yet been rearmed,
493+
// let's consider its next firing time is invalid for now.
494+
// It may be set to a relevant time in the future once
495+
// we scanned through the whole list of timeouts and if
496+
// we find a timeout that needs to expire.
497+
unrefTimer.when = -1;
489498

490-
if (diff < first._idleTimeout) {
491-
diff = first._idleTimeout - diff;
492-
unrefTimer.start(diff, 0);
493-
unrefTimer.when = now + diff;
494-
debug('unrefTimer rescheudling for later');
495-
return;
499+
// Iterate over the list of timeouts,
500+
// call the onTimeout callback for those expired,
501+
// and rearm the actual timer if the next timeout to expire
502+
// will expire before the current actual timer.
503+
var cur = unrefList._idlePrev;
504+
while (cur != unrefList) {
505+
timeSinceLastActive = now - cur._idleStart;
506+
507+
if (timeSinceLastActive < cur._idleTimeout) {
508+
// This timer hasn't expired yet, but check if its expiring time is
509+
// earlier than the actual timer's expiring time
510+
511+
nextTimeoutDuration = cur._idleTimeout - timeSinceLastActive;
512+
nextTimeoutTime = now + nextTimeoutDuration;
513+
if (minNextTimeoutTime == null ||
514+
(nextTimeoutTime < minNextTimeoutTime)) {
515+
// We found a timeout that will expire earlier,
516+
// store its next timeout time now so that we
517+
// can rearm the actual timer accordingly when
518+
// we scanned through the whole list.
519+
minNextTimeoutTime = nextTimeoutTime;
520+
}
521+
522+
// This timer hasn't expired yet, skipping
523+
cur = cur._idlePrev;
524+
continue;
496525
}
497526

498-
L.remove(first);
527+
// We found a timer that expired
528+
var domain = cur.domain;
499529

500-
domain = first.domain;
530+
if (!cur._onTimeout) continue;
501531

502-
if (!first._onTimeout) continue;
503-
if (domain && domain._disposed) continue;
532+
if (domain && domain._disposed)
533+
continue;
504534

505535
try {
536+
var threw = true;
537+
506538
if (domain) domain.enter();
507-
threw = true;
539+
540+
itemToDelete = cur;
541+
// Move to the previous item before calling the _onTimeout callback,
542+
// as it can mutate the list.
543+
cur = cur._idlePrev;
544+
545+
// Remove the timeout from the list because it expired.
546+
L.remove(itemToDelete);
547+
508548
debug('unreftimer firing timeout');
509-
first._called = true;
510-
first._onTimeout();
549+
itemToDelete._called = true;
550+
itemToDelete._onTimeout();
551+
511552
threw = false;
553+
512554
if (domain)
513555
domain.exit();
514556
} finally {
515557
if (threw) process.nextTick(unrefTimeout);
516558
}
517559
}
518560

519-
debug('unrefList is empty');
520-
unrefTimer.when = -1;
561+
// Rearm the actual timer with the timeout delay
562+
// of the earliest timeout found.
563+
if (minNextTimeoutTime != null) {
564+
unrefTimer.start(minNextTimeoutTime - now, 0);
565+
unrefTimer.when = minNextTimeoutTime;
566+
debug('unrefTimer rescheduled');
567+
} else if (L.isEmpty(unrefList)) {
568+
debug('unrefList is empty');
569+
}
521570
}
522571

523572

@@ -543,38 +592,14 @@ exports._unrefActive = function(item) {
543592
var now = Timer.now();
544593
item._idleStart = now;
545594

546-
if (L.isEmpty(unrefList)) {
547-
debug('unrefList empty');
548-
L.append(unrefList, item);
595+
var when = now + msecs;
549596

597+
// If the actual timer is set to fire too late, or not set to fire at all,
598+
// we need to make it fire earlier
599+
if (unrefTimer.when === -1 || unrefTimer.when > when) {
550600
unrefTimer.start(msecs, 0);
551-
unrefTimer.when = now + msecs;
601+
unrefTimer.when = when;
552602
debug('unrefTimer scheduled');
553-
return;
554-
}
555-
556-
var when = now + msecs;
557-
558-
debug('unrefList find where we can insert');
559-
560-
var cur, them;
561-
562-
for (cur = unrefList._idlePrev; cur != unrefList; cur = cur._idlePrev) {
563-
them = cur._idleStart + cur._idleTimeout;
564-
565-
if (when < them) {
566-
debug('unrefList inserting into middle of list');
567-
568-
L.append(cur, item);
569-
570-
if (unrefTimer.when > when) {
571-
debug('unrefTimer is scheduled to fire too late, reschedule');
572-
unrefTimer.start(msecs, 0);
573-
unrefTimer.when = when;
574-
}
575-
576-
return;
577-
}
578603
}
579604

580605
debug('unrefList append to end');
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
/*
4+
* This test is aimed at making sure that unref timers queued with
5+
* timers._unrefActive work correctly.
6+
*
7+
* Basically, it queues one timer in the unref queue, and then queues
8+
* it again each time its timeout callback is fired until the callback
9+
* has been called ten times.
10+
*
11+
* At that point, it unenrolls the unref timer so that its timeout callback
12+
* is not fired ever again.
13+
*
14+
* Finally, a ref timeout is used with a delay large enough to make sure that
15+
* all 10 timeouts had the time to expire.
16+
*/
17+
18+
const common = require('../common');
19+
const timers = require('timers');
20+
const assert = require('assert');
21+
22+
var someObject = {};
23+
var nbTimeouts = 0;
24+
25+
/*
26+
* libuv 0.10.x uses GetTickCount on Windows to implement timers, which uses
27+
* system's timers whose resolution is between 10 and 16ms. See
28+
* http://msdn.microsoft.com/en-us/library/windows/desktop/ms724408.aspx
29+
* for more information. That's the lowest resolution for timers across all
30+
* supported platforms. We're using it as the lowest common denominator,
31+
* and thus expect 5 timers to be able to fire in under 100 ms.
32+
*/
33+
const N = 5;
34+
const TEST_DURATION = 100;
35+
36+
timers.unenroll(someObject);
37+
timers.enroll(someObject, 1);
38+
39+
someObject._onTimeout = function _onTimeout() {
40+
++nbTimeouts;
41+
42+
if (nbTimeouts === N) timers.unenroll(someObject);
43+
44+
timers._unrefActive(someObject);
45+
};
46+
47+
timers._unrefActive(someObject);
48+
49+
setTimeout(function() {
50+
assert.equal(nbTimeouts, N);
51+
}, TEST_DURATION);

0 commit comments

Comments
 (0)