From ebc9130b276d417b61c82121ff75e06983bdbd7f Mon Sep 17 00:00:00 2001 From: Rick Waldron Date: Tue, 3 Oct 2017 15:24:19 -0400 Subject: [PATCH] Optimize clearing by marking entry.isRunnable --- Gruntfile.js | 5 +- README.md | 4 +- lib/temporal.js | 92 +++++++++++++++---------- package.json | 3 +- test/common/bootstrap.js | 3 + test/test.js | 140 ++++++++++++++++++++++++++++++++++++--- 6 files changed, 198 insertions(+), 49 deletions(-) create mode 100644 test/common/bootstrap.js diff --git a/Gruntfile.js b/Gruntfile.js index 6633f36..fce2f36 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -12,7 +12,10 @@ module.exports = function(grunt) { grunt.initConfig({ pkg: "", nodeunit: { - files: ["test/**/*.js"] + files: [ + "test/common/bootstrap.js", + "test/**/*.js" + ] }, jshint: { all: { diff --git a/README.md b/README.md index 62b15c0..1e31d91 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,11 @@ temporal.delay(500, function() { // Add a task with an id temporal.delay({ + id: "myTask", time:500, - operation: function() { + task: function() { console.log("500ms later..."); }), - id: "myTask" }); // Selectively delete tasks diff --git a/lib/temporal.js b/lib/temporal.js index 078a7c3..44b78ed 100644 --- a/lib/temporal.js +++ b/lib/temporal.js @@ -1,3 +1,5 @@ +"use strict"; + var Emitter = require("events").EventEmitter; var util = require("util"); @@ -17,9 +19,12 @@ var isProcessing = false; // Queue emitters are stored privately in a Map to avoid using // |this| alias patterns. var priv = new Map(); +var taskId = 0; -var tick = global.setImmediate || process.nextTick; +// Entries stored by id +var entriesById = new Map(); +var tick = global.setImmediate || process.nextTick; /** * Task create a temporal task item * @param {Object} entry Options for entry {time, task} @@ -31,10 +36,7 @@ function Task(entry) { this.called = 0; this.now = this.calledAt = Date.now(); - - if (entry.id) { - this.id = entry.id; - } + this.id = entry.id || ++taskId; priv.set(this, entry); @@ -42,13 +44,17 @@ function Task(entry) { entry.isRunnable = true; entry.later = this.now + entry.time; - if (!queue[entry.later]) { queue[entry.later] = []; } - // console.log( entry.later, this ); queue[entry.later].push(this); + + if (!entriesById.has(this.id)) { + entriesById.set(this.id, []); + } + + entriesById.get(this.id).push(entry); } // Inherit EventEmitter API @@ -231,33 +237,32 @@ function processQueue() { } ["loop", "delay"].forEach(function(type) { - exportable[type] = function(time, operation) { - - var opts; + exportable[type] = function(time, task) { + var options; if (typeof time === "function") { - operation = time; + task = time; time = 10; } if (typeof time === "object") { - opts = time; - opts.type = type; + options = time; + options.type = type; } else { - opts = { - time: time, - type: type, - task: operation + options = { + time, + type, + task, }; } - var task = new Task(opts); + var instance = new Task(options); if (!isProcessing) { processQueue(); } - return task; + return instance; }; }); @@ -278,32 +283,49 @@ exportable.repeat = function(n, ms, callback) { }); }; -exportable.clear = function(id) { +exportable.clear = function(input) { - if (typeof id === "undefined") { + var id; + + if (typeof input === "undefined") { isProcessing = false; queue = {}; } else { - queue = filterQueue(queue, id); + id = input instanceof Task ? input.id : input; + + let entries = entriesById.get(id); + + if (entries) { + for (let entry of entries) { + entry.isRunnable = false; + } + } + entriesById.delete(id); } - if (Object.keys(queue).length === 0) { - exportable.removeAllListeners(); + if (entriesById.size === 0) { + isProcessing = false; + queue = {}; } + if (isEmpty(queue)) { + exportable.removeAllListeners(); + } }; -function filterQueue(queue, id) { - var filtered = {}; - Object.keys(queue).forEach(function(key) { - queue[key] = queue[key].filter(function(entry) { - return entry.id !== id; - }); - if (queue[key].length > 0) { - filtered[key] = queue[key]; - } - }); - return filtered; +function isEmpty(o) { + for (let p in o) { + return (p, false); + } + return true; +} + +if (global.TEST_ENV) { + exportable.test = { + get entriesById() { + return entriesById; + }, + }; } module.exports = exportable; diff --git a/package.json b/package.json index da0db89..4bf15ec 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "grunt-contrib-jshint": "latest", "grunt-contrib-nodeunit": "latest", "grunt-contrib-watch": "latest", - "grunt-jsbeautifier": "latest" + "grunt-jsbeautifier": "latest", + "sinon": "^4.0.0" }, "keywords": [ "schedule", diff --git a/test/common/bootstrap.js b/test/common/bootstrap.js new file mode 100644 index 0000000..9d9d7e7 --- /dev/null +++ b/test/common/bootstrap.js @@ -0,0 +1,3 @@ +"use strict"; + +global.TEST_ENV = true; diff --git a/test/test.js b/test/test.js index 1c05cf9..26f0f69 100644 --- a/test/test.js +++ b/test/test.js @@ -1,7 +1,10 @@ "use strict"; +require("./common/bootstrap"); + var temporal = require("../lib/temporal.js"); var Emitter = require("events").EventEmitter; +var sinon = require("sinon"); function sum(a, b) { @@ -14,6 +17,11 @@ function fuzzy(actual, expected, tolerance) { } +exports.setUp = function(done) { + temporal.test.entriesById.clear(); + done(); +}; + exports["temporal"] = { setUp: function(done) { done(); @@ -79,39 +87,80 @@ exports["context"] = { exports["clear"] = { setUp: function(done) { + this.sandbox = sinon.sandbox.create(); + this.sandbox.spy(temporal, "removeAllListeners"); done(); }, tearDown: function(done) { + this.sandbox.restore(); temporal.clear(); done(); }, - clear: function(test) { - test.expect(1); + clearAllWithNoId: function(test) { + test.expect(2); temporal.wait(20, function() { // this will never happen. - console.log("shouldn't happen"); test.ok(false); }); temporal.wait(10, function() { - console.log("kill it"); + // this will never happen. + test.ok(false); }); - setTimeout(function() { + setTimeout(() => { temporal.clear(); + test.equal(temporal.removeAllListeners.callCount, 1); + test.ok(true); + test.done(); + }, 1); + }, + + clearAllWithId: function(test) { + test.expect(2); + + temporal.delay({ + time: 5, + id: "clearAllWithId", + task: function() { + // Should never be reached + test.ok(false); + } + }); + temporal.delay({ + time: 10, + id: "clearAllWithId", + task: function() { + // Should never be reached + test.ok(false); + } + }); + temporal.delay({ + time: 15, + id: "clearAllWithId", + task: function() { + // Should never be reached + test.ok(false); + } + }); + + setTimeout(() => { + temporal.clear("clearAllWithId"); + test.equal(temporal.removeAllListeners.callCount, 1); test.ok(true); test.done(); }, 1); }, + clearById: function(test) { - test.expect(1); + test.expect(2); temporal.delay({ time: 10, id: "myId", - operation: function() { - // this should not happen. + task: function() { + // Should never be reached console.log("kill it"); test.ok(false); } @@ -123,11 +172,81 @@ exports["clear"] = { test.done(); }); - setTimeout(function() { + setTimeout(() => { temporal.clear("myId"); + test.equal(temporal.removeAllListeners.callCount, 0); }, 1); - } + }, + + clearByTaskInstance: function(test) { + test.expect(2); + + var task = temporal.delay({ + time: 10, + id: "foo", + task: function() { + // Should never be reached + test.ok(false); + } + }); + temporal.delay(15, function() { + // this should happen. + test.ok(true); + test.done(); + }); + + setTimeout(() => { + temporal.clear(task); + test.equal(temporal.removeAllListeners.callCount, 0); + }, 1); + }, + + clearMultipleById: function(test) { + test.expect(2); + + var id = "foo"; + + temporal.delay({ + time: 10, + id: id, + task: function() { + // Should never be reached + test.ok(false); + } + }); + + temporal.delay({ + time: 20, + id: id, + task: function() { + // Should never be reached + test.ok(false); + } + }); + + temporal.delay({ + time: 30, + id: id, + task: function() { + // Should never be reached + test.ok(false); + } + }); + + temporal.delay(40, function() { + // this should happen. + test.ok(true); + test.done(); + }); + + setTimeout(() => { + temporal.clear(id); + test.equal(temporal.removeAllListeners.callCount, 0); + }, 1); + + }, + }; exports["loops"] = { @@ -191,6 +310,7 @@ exports["delay"] = { temporal.clear(); done(); }, + delay: function(test) { test.expect(13);