diff --git a/lib/timers.js b/lib/timers.js index c1b8a0291f..d0ec0c8d9d 100644 --- a/lib/timers.js +++ b/lib/timers.js @@ -9,24 +9,25 @@ if (process.env.NODE_debug && /timer/.test(process.env.NODE_debug)) { } -// IDLE TIMEOUTS -// -// Because often many sockets will have the same idle timeout we will not -// use one timeout watcher per item. It is too much overhead. Instead -// we'll use a single watcher for all sockets with the same timeout value -// and a linked list. This technique is described in the libev manual: -// http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#Be_smart_about_timeouts +// Export the linklist code for testing. + + +exports.linkedList = {}; + + +function init(list) { + list._idleNext = list; + list._idlePrev = list; +} +exports.linkedList.init = init; -// Object containing all lists, timers -// key = time in milliseconds -// value = list -var lists = {}; // show the most idle item function peek(list) { if (list._idlePrev == list) return null; return list._idlePrev; } +exports.linkedList.peek = peek; // remove the most idle item from the list @@ -35,22 +36,54 @@ function shift(list) { remove(first); return first; } +exports.linkedList.shift = shift; // remove a item from its list function remove(item) { - item._idleNext._idlePrev = item._idlePrev; - item._idlePrev._idleNext = item._idleNext; + if (item._idleNext) { + item._idleNext._idlePrev = item._idlePrev; + } + + if (item._idlePrev) { + item._idlePrev._idleNext = item._idleNext; + } + + item._idleNext = null; + item._idlePrev = null; } +exports.linkedList.remove = remove; // remove a item from its list and place at the end. function append(list, item) { + remove(item); item._idleNext = list._idleNext; list._idleNext._idlePrev = item; item._idlePrev = list; list._idleNext = item; } +exports.linkedList.append = append; + + +function isEmpty(list) { + return list._idleNext === list; +} +exports.linkedList.isEmpty = isEmpty; + + +// IDLE TIMEOUTS +// +// Because often many sockets will have the same idle timeout we will not +// use one timeout watcher per item. It is too much overhead. Instead +// we'll use a single watcher for all sockets with the same timeout value +// and a linked list. This technique is described in the libev manual: +// http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#Be_smart_about_timeouts + +// Object containing all lists, timers +// key = time in milliseconds +// value = list +var lists = {}; // the main function - creates lists on demand and the watchers associated @@ -67,8 +100,7 @@ function insert(item, msecs) { list = lists[msecs]; } else { list = new Timer(); - list._idleNext = list; - list._idlePrev = list; + init(list); lists[msecs] = list; @@ -78,6 +110,7 @@ function insert(item, msecs) { // just set its repeat var now = new Date(); debug('now: ' + now); + var first; while (first = peek(list)) { var diff = now - first._idleStart; @@ -91,8 +124,9 @@ function insert(item, msecs) { if (first._onTimeout) first._onTimeout(); } } + debug(msecs + ' list empty'); - assert(list._idleNext === list); // list is empty + assert(isEmpty(list)); list.stop(); }; } @@ -103,7 +137,7 @@ function insert(item, msecs) { } append(list, item); - assert(list._idleNext !== list); // list is not empty + assert(!isEmpty(list)); // list is not empty } @@ -114,7 +148,7 @@ var unenroll = exports.unenroll = function(item) { var list = lists[item._idleTimeout]; // if empty then stop the watcher debug('unenroll'); - if (list && list._idlePrev == list) { + if (list && isEmpty(list)) { debug('unenroll: list empty'); list.stop(); } @@ -129,8 +163,7 @@ exports.enroll = function(item, msecs) { if (item._idleNext) unenroll(item); item._idleTimeout = msecs; - item._idleNext = item; - item._idlePrev = item; + init(item); }; // call this whenever the item is active (not idle) @@ -142,14 +175,8 @@ exports.active = function(item) { if (item._idleNext == item) { insert(item, msecs); } else { - // inline append item._idleStart = new Date(); - item._idleNext._idlePrev = item._idlePrev; - item._idlePrev._idleNext = item._idleNext; - item._idleNext = list._idleNext; - item._idleNext._idlePrev = item; - item._idlePrev = list; - list._idleNext = item; + append(list, item); } } }; diff --git a/test/simple/test-timers-linked-list.js b/test/simple/test-timers-linked-list.js new file mode 100644 index 0000000000..9caa909d01 --- /dev/null +++ b/test/simple/test-timers-linked-list.js @@ -0,0 +1,94 @@ +var common = require('../common'); +var assert = require('assert'); +var L = require('timers').linkedList; + + +var list = { name: "list" }; +var A = { name: "A" }; +var B = { name: "B" }; +var C = { name: "C" }; +var D = { name: "D" }; + + +L.init(list); +assert.ok(L.isEmpty(list)); +assert.equal(null, L.peek(list)); + +L.append(list, A); +// list -> A +assert.equal(A, L.peek(list)); + +L.append(list, B); +// list -> A -> B +assert.equal(A, L.peek(list)); + +L.append(list, C); +// list -> A -> B -> C +assert.equal(A, L.peek(list)); + +L.append(list, D); +// list -> A -> B -> C -> D +assert.equal(A, L.peek(list)); + +var x = L.shift(list); +assert.equal(A, x); +// list -> B -> C -> D +assert.equal(B, L.peek(list)); + +x = L.shift(list); +assert.equal(B, x); +// list -> C -> D +assert.equal(C, L.peek(list)); + +// B is already removed, so removing it again shouldn't hurt. +L.remove(B); +// list -> C -> D +assert.equal(C, L.peek(list)); + +// Put B back on the list +L.append(list, B); +// list -> C -> D -> B +assert.equal(C, L.peek(list)); + +L.remove(C); +// list -> D -> B +assert.equal(D, L.peek(list)); + +L.remove(B); +// list -> D +assert.equal(D, L.peek(list)); + +L.remove(D); +// list +assert.equal(null, L.peek(list)); + + +assert.ok(L.isEmpty(list)); + + +L.append(list, D); +// list -> D +assert.equal(D, L.peek(list)); + +L.append(list, C); +L.append(list, B); +L.append(list, A); +// list -> D -> C -> B -> A + +// Append should REMOVE C from the list and append it to the end. +L.append(list, C); + +// list -> D -> B -> A -> C +assert.equal(D, L.shift(list)); +// list -> B -> A -> C +assert.equal(B, L.peek(list)); +assert.equal(B, L.shift(list)); +// list -> A -> C +assert.equal(A, L.peek(list)); +assert.equal(A, L.shift(list)); +// list -> C +assert.equal(C, L.peek(list)); +assert.equal(C, L.shift(list)); +// list +assert.ok(L.isEmpty(list)); +