Sets and executes timeouts set during a tick.
All timeouts and intervals set during a tick were being scheduled to run at delay + end-of-tick, instead of delay + time-of-outer-timeout. Scheduled run-at times were shifted because currentTime was being incremented before executing scheduled functions. Additionally, the execute loop was iterating over a functions-to-run array, created from scheduledFunctions before starting. Any changes to scheduledFunctions were being ignored during the tick, and the next tick would ignore any functions which should have been executed in the past. The commit is a rewrite of DelayedFunctionScheduler, preserving the public interface. Execution of scheduled functions updates currentTime on each iteration, and each time takes the functions with the lowest runAtMillis from the schedule, if they aren't higher than endTime.
This commit is contained in:
committed by
Sheel Choksi
parent
8a6d7828c6
commit
c78fba4b13
@@ -341,4 +341,19 @@ describe("Clock (acceptance)", function() {
|
|||||||
clock.tick();
|
clock.tick();
|
||||||
expect(delayedFn2).toHaveBeenCalled();
|
expect(delayedFn2).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("correctly calls functions scheduled while the Clock is advancing", function() {
|
||||||
|
var delayedFn1 = jasmine.createSpy('delayedFn1'),
|
||||||
|
delayedFn2 = jasmine.createSpy('delayedFn2'),
|
||||||
|
delayedFunctionScheduler = new j$.DelayedFunctionScheduler(),
|
||||||
|
clock = new j$.Clock({setTimeout: function() {}}, delayedFunctionScheduler);
|
||||||
|
|
||||||
|
delayedFn1.and.callFake(function() { clock.setTimeout(delayedFn2, 1); });
|
||||||
|
clock.install();
|
||||||
|
clock.setTimeout(delayedFn1, 5);
|
||||||
|
|
||||||
|
clock.tick(6);
|
||||||
|
expect(delayedFn1).toHaveBeenCalled();
|
||||||
|
expect(delayedFn2).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -175,5 +175,72 @@ describe("DelayedFunctionScheduler", function() {
|
|||||||
expect(fn).toHaveBeenCalled();
|
expect(fn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("schedules a function for later execution during a tick", function () {
|
||||||
|
var scheduler = new j$.DelayedFunctionScheduler(),
|
||||||
|
fn = jasmine.createSpy('fn'),
|
||||||
|
fnDelay = 10;
|
||||||
|
|
||||||
|
scheduler.scheduleFunction(function () {
|
||||||
|
scheduler.scheduleFunction(fn, fnDelay);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
scheduler.tick(fnDelay);
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("#removeFunctionWithId removes a previously scheduled function with a given id during a tick", function () {
|
||||||
|
var scheduler = new j$.DelayedFunctionScheduler(),
|
||||||
|
fn = jasmine.createSpy('fn'),
|
||||||
|
fnDelay = 10,
|
||||||
|
timeoutKey;
|
||||||
|
|
||||||
|
scheduler.scheduleFunction(function () {
|
||||||
|
scheduler.removeFunctionWithId(timeoutKey);
|
||||||
|
}, 0);
|
||||||
|
timeoutKey = scheduler.scheduleFunction(fn, fnDelay);
|
||||||
|
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
scheduler.tick(fnDelay);
|
||||||
|
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("executes recurring functions interleaved with regular functions and functions scheduled during a tick in the correct order", function () {
|
||||||
|
var scheduler = new j$.DelayedFunctionScheduler(),
|
||||||
|
fn = jasmine.createSpy('fn'),
|
||||||
|
recurringCallCount = 0,
|
||||||
|
recurring = jasmine.createSpy('recurring').and.callFake(function() {
|
||||||
|
recurringCallCount++;
|
||||||
|
if (recurringCallCount < 5) {
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
innerFn = jasmine.createSpy('innerFn').and.callFake(function() {
|
||||||
|
expect(recurring.calls.count()).toBe(4);
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
}),
|
||||||
|
scheduling = jasmine.createSpy('scheduling').and.callFake(function() {
|
||||||
|
expect(recurring.calls.count()).toBe(3);
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
scheduler.scheduleFunction(innerFn, 10); // 41ms absolute
|
||||||
|
});
|
||||||
|
|
||||||
|
scheduler.scheduleFunction(recurring, 10, [], true);
|
||||||
|
scheduler.scheduleFunction(fn, 50);
|
||||||
|
scheduler.scheduleFunction(scheduling, 31);
|
||||||
|
|
||||||
|
scheduler.tick(60);
|
||||||
|
|
||||||
|
expect(recurring).toHaveBeenCalled();
|
||||||
|
expect(recurring.calls.count()).toBe(6);
|
||||||
|
expect(fn).toHaveBeenCalled();
|
||||||
|
expect(scheduling).toHaveBeenCalled();
|
||||||
|
expect(innerFn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
getJasmineRequireObj().DelayedFunctionScheduler = function() {
|
getJasmineRequireObj().DelayedFunctionScheduler = function() {
|
||||||
function DelayedFunctionScheduler() {
|
function DelayedFunctionScheduler() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var scheduledLookup = [];
|
||||||
var scheduledFunctions = {};
|
var scheduledFunctions = {};
|
||||||
var currentTime = 0;
|
var currentTime = 0;
|
||||||
var delayedFnCount = 0;
|
var delayedFnCount = 0;
|
||||||
|
|
||||||
self.tick = function(millis) {
|
self.tick = function(millis) {
|
||||||
millis = millis || 0;
|
millis = millis || 0;
|
||||||
currentTime = currentTime + millis;
|
var endTime = currentTime + millis;
|
||||||
runFunctionsWithinRange(currentTime - millis, currentTime);
|
|
||||||
|
runScheduledFunctions(endTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
|
self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
|
||||||
@@ -24,7 +26,8 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() {
|
|||||||
millis = millis || 0;
|
millis = millis || 0;
|
||||||
timeoutKey = timeoutKey || ++delayedFnCount;
|
timeoutKey = timeoutKey || ++delayedFnCount;
|
||||||
runAtMillis = runAtMillis || (currentTime + millis);
|
runAtMillis = runAtMillis || (currentTime + millis);
|
||||||
scheduledFunctions[timeoutKey] = {
|
|
||||||
|
var funcToSchedule = {
|
||||||
runAtMillis: runAtMillis,
|
runAtMillis: runAtMillis,
|
||||||
funcToCall: f,
|
funcToCall: f,
|
||||||
recurring: recurring,
|
recurring: recurring,
|
||||||
@@ -32,58 +35,73 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() {
|
|||||||
timeoutKey: timeoutKey,
|
timeoutKey: timeoutKey,
|
||||||
millis: millis
|
millis: millis
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (runAtMillis in scheduledFunctions) {
|
||||||
|
scheduledFunctions[runAtMillis].push(funcToSchedule);
|
||||||
|
} else {
|
||||||
|
scheduledFunctions[runAtMillis] = [funcToSchedule];
|
||||||
|
scheduledLookup.push(runAtMillis);
|
||||||
|
scheduledLookup.sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return timeoutKey;
|
return timeoutKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.removeFunctionWithId = function(timeoutKey) {
|
self.removeFunctionWithId = function(timeoutKey) {
|
||||||
delete scheduledFunctions[timeoutKey];
|
for (var runAtMillis in scheduledFunctions) {
|
||||||
|
var funcs = scheduledFunctions[runAtMillis];
|
||||||
|
var i = indexOfFirstToPass(funcs, function (func) {
|
||||||
|
return func.timeoutKey === timeoutKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i > -1) {
|
||||||
|
if (funcs.length === 1) {
|
||||||
|
delete scheduledFunctions[runAtMillis];
|
||||||
|
deleteFromLookup(runAtMillis);
|
||||||
|
} else {
|
||||||
|
funcs.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// intervals get rescheduled when executed, so there's never more
|
||||||
|
// than a single scheduled function with a given timeoutKey
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.reset = function() {
|
self.reset = function() {
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
|
scheduledLookup = [];
|
||||||
scheduledFunctions = {};
|
scheduledFunctions = {};
|
||||||
delayedFnCount = 0;
|
delayedFnCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
|
|
||||||
// finds/dupes functions within range and removes them.
|
function indexOfFirstToPass(array, testFn) {
|
||||||
function functionsWithinRange(startMillis, endMillis) {
|
var index = -1;
|
||||||
var fnsToRun = [];
|
|
||||||
for (var timeoutKey in scheduledFunctions) {
|
|
||||||
var scheduledFunc = scheduledFunctions[timeoutKey];
|
|
||||||
if (scheduledFunc &&
|
|
||||||
scheduledFunc.runAtMillis >= startMillis &&
|
|
||||||
scheduledFunc.runAtMillis <= endMillis) {
|
|
||||||
|
|
||||||
// remove fn -- we'll reschedule later if it is recurring.
|
for (var i = 0; i < array.length; ++i) {
|
||||||
self.removeFunctionWithId(timeoutKey);
|
if (testFn(array[i])) {
|
||||||
if (!scheduledFunc.recurring) {
|
index = i;
|
||||||
fnsToRun.push(scheduledFunc); // schedules each function only once
|
break;
|
||||||
} else {
|
|
||||||
fnsToRun.push(buildNthInstanceOf(scheduledFunc, 0));
|
|
||||||
var additionalTimesFnRunsInRange =
|
|
||||||
Math.floor((endMillis - scheduledFunc.runAtMillis) / scheduledFunc.millis);
|
|
||||||
for (var i = 0; i < additionalTimesFnRunsInRange; i++) {
|
|
||||||
fnsToRun.push(buildNthInstanceOf(scheduledFunc, i + 1));
|
|
||||||
}
|
|
||||||
reschedule(buildNthInstanceOf(scheduledFunc, additionalTimesFnRunsInRange));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fnsToRun;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNthInstanceOf(scheduledFunc, n) {
|
function deleteFromLookup(key) {
|
||||||
return {
|
var value = Number(key);
|
||||||
runAtMillis: scheduledFunc.runAtMillis + (scheduledFunc.millis * n),
|
var i = indexOfFirstToPass(scheduledLookup, function (millis) {
|
||||||
funcToCall: scheduledFunc.funcToCall,
|
return millis === value;
|
||||||
params: scheduledFunc.params,
|
});
|
||||||
millis: scheduledFunc.millis,
|
|
||||||
recurring: scheduledFunc.recurring,
|
if (i > -1) {
|
||||||
timeoutKey: scheduledFunc.timeoutKey
|
scheduledLookup.splice(i, 1);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reschedule(scheduledFn) {
|
function reschedule(scheduledFn) {
|
||||||
@@ -95,21 +113,32 @@ getJasmineRequireObj().DelayedFunctionScheduler = function() {
|
|||||||
scheduledFn.runAtMillis + scheduledFn.millis);
|
scheduledFn.runAtMillis + scheduledFn.millis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runScheduledFunctions(endTime) {
|
||||||
function runFunctionsWithinRange(startMillis, endMillis) {
|
if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
|
||||||
var funcsToRun = functionsWithinRange(startMillis, endMillis);
|
|
||||||
if (funcsToRun.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
funcsToRun.sort(function(a, b) {
|
do {
|
||||||
return a.runAtMillis - b.runAtMillis;
|
currentTime = scheduledLookup.shift();
|
||||||
});
|
|
||||||
|
|
||||||
for (var i = 0; i < funcsToRun.length; ++i) {
|
var funcsToRun = scheduledFunctions[currentTime];
|
||||||
var funcToRun = funcsToRun[i];
|
delete scheduledFunctions[currentTime];
|
||||||
funcToRun.funcToCall.apply(null, funcToRun.params || []);
|
|
||||||
}
|
for (var i = 0; i < funcsToRun.length; ++i) {
|
||||||
|
var funcToRun = funcsToRun[i];
|
||||||
|
funcToRun.funcToCall.apply(null, funcToRun.params || []);
|
||||||
|
|
||||||
|
if (funcToRun.recurring) {
|
||||||
|
reschedule(funcToRun);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (scheduledLookup.length > 0 &&
|
||||||
|
// checking first if we're out of time prevents setTimeout(0)
|
||||||
|
// scheduled in a funcToRun from forcing an extra iteration
|
||||||
|
currentTime !== endTime &&
|
||||||
|
scheduledLookup[0] <= endTime);
|
||||||
|
|
||||||
|
currentTime = endTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user