-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize timer._unrefActive #268
Comments
I don't think that splitting it into two lists is going to help much, unless you want to create a timer handle per tcp/udp/pipe handle. There is currently a lot of low hanging fruit in the timers module, it just needs someone that wants to work on it for a few days. For example, .unref() always creates a new timer handle but that's not strictly necessary; the module can just as easily maintain a global refcount and ref/unref the main timer handle. Another probable win is the introduction of a timer wheel. In most applications, the majority of timers are short-lived and never expire because the predominant use case is to time out network I/O after a good amount of time has elapsed. Most I/O however completes in milliseconds and afterwards the timer is deleted or reset. A reset is when the timeout is extended for (for example) another 30 seconds after a read or write. With the current implementation, resetting consists of a O(1) deletion and O(n) insertion, where As an intermediate step, switching to binary search would lower the overhead to O(log n) for insertion and deletion. It's probably just as much work to implement binary search as it is to switch to a timer wheel, however. The one (albeit minor) downside to a timer wheel is that you have to rehash timers every now and then. However, the beauty of it is that most timers are repeatedly reset and those always stay in the highest bucket, they are never rehashed. Yet another optimization path is to try reduce the number of calls to Date.now() / Timer.now(). Obtaining the current system time is often outrageously expensive in virtual machines because the RDTSC instruction or HPET access or whatever the time source is, is intercepted and emulated by the host. It's not uncommon to see upwards of 20% wall clock time being wasted in clock_gettime() and gettimeofday() system calls. A harebrained idea I had is to start a thread that consists of a loop that wakes up every millisecond, calls clock_gettime() and writes the result to an atomic variable. The main thread then has cheap but accurate time; the penalty is offloaded to the other thread. |
Trying to take a look at this, did some reading on timer wheels. [1] [2] (that being said, our implementation with handles means i'm still a bit confused. :))
Don't we already have two?
Not strictly, unless the timer wheel is very large & more than one timers never fire on the same tick. Also, I'm not really sure how this would get rid of the aforementioned
How would this effect deletion? This does seem like an obvious win for setting timers though. But, we still have the two lists. |
https://gist.github.com/Fishrock123/23950d26346dc8077a08 @bnoordhuis Even in cases that should be horrible (to my best understanding) for O(n) on timeout (Tests 3 & 4), it still performs over 2x as well as a sorted array. At minimum, I move we accept the joyent/node patches for it. The heap impl (primarly made by Ben) is probably usable even in this state without severe optimizations, it seems generally better for the common case, and consistent even to the uncommon case (lots of timeouts). |
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 <tjfontaine@gmail.com> Conflicts: lib/timers.js Fixes: nodejs/node-convergence-archive#23 Ref: #268 PR-URL: #2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Commit 934bfe2 had introduced a regression where node would crash trying to access a null unref timer if a given unref timer's callback would remove other unref timers set to fire in the future. More generally, it makes the unrefTimeout function more solid by not mutating the unrefList while traversing it. Fixes: nodejs/node-v0.x-archive#8897 Conflicts: lib/timers.js Fixes: nodejs/node-convergence-archive#23 Ref: #268 PR-URL: #2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
This commit addresses most of the review comments in #2540, which are kept in this separate commit so as to better preserve the prior two patches as they landed in 0.12. This commit: - Fixes a bug with unrefActive timers and disposed domains. - Fixes a bug with unrolling an unrefActive timer from another. - Adds a test for both above bugs. - Improves check logic, making it stricter, simpler, or both. - Optimizes nicer with a smaller, separate function for the try/catch. Fixes: nodejs/node-convergence-archive#23 Ref: #268 PR-URL: #2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Closing while keeping in mind that I can try to optimize it further. When I have some time. |
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 <tjfontaine@gmail.com> Conflicts: lib/timers.js Fixes: nodejs/node-convergence-archive#23 Ref: #268 PR-URL: #2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Commit 934bfe2 had introduced a regression where node would crash trying to access a null unref timer if a given unref timer's callback would remove other unref timers set to fire in the future. More generally, it makes the unrefTimeout function more solid by not mutating the unrefList while traversing it. Fixes: nodejs/node-v0.x-archive#8897 Conflicts: lib/timers.js Fixes: nodejs/node-convergence-archive#23 Ref: #268 PR-URL: #2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
This commit addresses most of the review comments in #2540, which are kept in this separate commit so as to better preserve the prior two patches as they landed in 0.12. This commit: - Fixes a bug with unrefActive timers and disposed domains. - Fixes a bug with unrolling an unrefActive timer from another. - Adds a test for both above bugs. - Improves check logic, making it stricter, simpler, or both. - Optimizes nicer with a smaller, separate function for the try/catch. Fixes: nodejs/node-convergence-archive#23 Ref: #268 PR-URL: #2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
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 <tjfontaine@gmail.com> Conflicts: lib/timers.js Fixes: nodejs/node-convergence-archive#23 Ref: nodejs#268 PR-URL: nodejs#2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
Commit 934bfe2 had introduced a regression where node would crash trying to access a null unref timer if a given unref timer's callback would remove other unref timers set to fire in the future. More generally, it makes the unrefTimeout function more solid by not mutating the unrefList while traversing it. Fixes: nodejs/node-v0.x-archive#8897 Conflicts: lib/timers.js Fixes: nodejs/node-convergence-archive#23 Ref: nodejs#268 PR-URL: nodejs#2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
This commit addresses most of the review comments in nodejs#2540, which are kept in this separate commit so as to better preserve the prior two patches as they landed in 0.12. This commit: - Fixes a bug with unrefActive timers and disposed domains. - Fixes a bug with unrolling an unrefActive timer from another. - Adds a test for both above bugs. - Improves check logic, making it stricter, simpler, or both. - Optimizes nicer with a smaller, separate function for the try/catch. Fixes: nodejs/node-convergence-archive#23 Ref: nodejs#268 PR-URL: nodejs#2540 Reviewed-By: bnoordhuis - Ben Noordhuis <info@bnoordhuis.nl>
@misterdjules pointed out that node had become significantly faster after optimizing timer._unrefActive(). To get the idea: https://github.com/joyent/node/commits/v0.10/lib/timers.js.
It turns out that timer._unrefActive() was added somewhere in 2013 to fix an issue related to unref()ing http requests; when an http request object is unref'ed, it's associated timeout timer also needs to be unref'ed or it'll keep the loop open. But it's really a performance bottleneck; _unrefActive() performs what is basically a linear scan of a linked list.
What seems more reasonable is to make timers that are stored in the linked list (through Timer.active()) always be unref'ed, and just use a separate TimerWrap object for all user timers that are scheduled through setTimeout/setInterval.
The text was updated successfully, but these errors were encountered: