scratch-vm/src/util/task-queue.js

163 lines
5.7 KiB
JavaScript
Raw Normal View History

2018-08-31 14:16:20 -04:00
const Timer = require('../util/timer');
/**
* This class uses the token bucket algorithm to control a queue of tasks.
*/
2018-09-14 22:34:34 -04:00
class TaskQueue {
2018-08-31 14:16:20 -04:00
/**
2018-09-14 22:34:34 -04:00
* Creates an instance of TaskQueue.
* To allow bursts, set `maxTokens` to several times the average task cost.
* To prevent bursts, set `maxTokens` to the cost of the largest tasks.
* Note that tasks with a cost greater than `maxTokens` will be rejected.
*
2018-08-31 14:16:20 -04:00
* @param {number} maxTokens - the maximum number of tokens in the bucket (burst size).
* @param {number} refillRate - the number of tokens to be added per second (sustain rate).
2018-09-14 22:34:34 -04:00
* @param {number} [startingTokens=maxTokens] - the number of tokens the bucket starts with.
* @memberof TaskQueue
2018-08-31 14:16:20 -04:00
*/
constructor (maxTokens, refillRate, startingTokens = maxTokens) {
this._maxTokens = maxTokens;
this._refillRate = refillRate;
this._pendingTaskRecords = [];
2018-08-31 14:16:20 -04:00
this._tokenCount = startingTokens;
this._timer = new Timer();
this._timer.start();
this._timeout = null;
2018-08-31 14:16:20 -04:00
this._lastUpdateTime = this._timer.timeElapsed();
}
/**
* Wait until the token bucket is full enough, then run the provided task.
*
* @param {Function} task - the task to run.
* @param {number} [cost=1] - the number of tokens this task consumes from the bucket.
* @returns {Promise} - a promise for the task's return value.
2018-09-14 22:34:34 -04:00
* @memberof TaskQueue
2018-08-31 14:16:20 -04:00
*/
do (task, cost = 1) {
const newRecord = {};
2018-08-31 14:16:20 -04:00
const promise = new Promise((resolve, reject) => {
newRecord.wrappedTask = () => {
2018-08-31 14:16:20 -04:00
const canRun = this._refillAndSpend(cost);
if (canRun) {
// Remove this task from the queue and run it
this._pendingTaskRecords.shift();
2018-08-31 14:16:20 -04:00
try {
resolve(task());
} catch (e) {
reject(e);
}
// Tell the next wrapper to start trying to run its task
if (this._pendingTaskRecords.length > 0) {
const nextRecord = this._pendingTaskRecords[0];
nextRecord.wrappedTask();
2018-08-31 14:16:20 -04:00
}
} else {
// This task can't run yet. Estimate when it will be able to, then try again.
newRecord.reject = reject;
this._waitUntilAffordable(cost).then(() => newRecord.wrappedTask());
2018-08-31 14:16:20 -04:00
}
};
});
this._pendingTaskRecords.push(newRecord);
2018-08-31 14:16:20 -04:00
if (this._pendingTaskRecords.length === 1) {
newRecord.wrappedTask();
2018-08-31 14:16:20 -04:00
}
return promise;
}
/**
* Cancel all pending tasks, rejecting all their promises.
2018-09-14 22:34:34 -04:00
*
* @memberof TaskQueue
*/
cancelAll () {
if (this._timeout !== null) {
clearTimeout(this._timeout);
this._timeout = null;
}
this._pendingTaskRecords.forEach(r => r.reject());
this._pendingTaskRecords = [];
}
2018-08-31 14:16:20 -04:00
/**
* Shorthand for calling @ _refill() then _spend(cost).
2018-09-14 22:34:34 -04:00
*
* @see {@link TaskQueue#_refill}
* @see {@link TaskQueue#_spend}
2018-08-31 14:16:20 -04:00
* @param {number} cost - the number of tokens to try to spend.
* @returns {boolean} true if we had enough tokens; false otherwise.
2018-09-14 22:34:34 -04:00
* @memberof TaskQueue
2018-08-31 14:16:20 -04:00
*/
_refillAndSpend (cost) {
this._refill();
return this._spend(cost);
}
/**
* Refill the token bucket based on the amount of time since the last refill.
2018-09-14 22:34:34 -04:00
*
* @memberof TaskQueue
2018-08-31 14:16:20 -04:00
*/
_refill () {
const now = this._timer.timeElapsed();
const timeSinceRefill = now - this._lastUpdateTime;
if (timeSinceRefill <= 0) return;
this._lastUpdateTime = now;
this._tokenCount += timeSinceRefill * this._refillRate / 1000;
this._tokenCount = Math.min(this._tokenCount, this._maxTokens);
}
/**
* If we can "afford" the given cost, subtract that many tokens and return true.
* Otherwise, return false.
2018-09-14 22:34:34 -04:00
*
2018-08-31 14:16:20 -04:00
* @param {number} cost - the number of tokens to try to spend.
* @returns {boolean} true if we had enough tokens; false otherwise.
2018-09-14 22:34:34 -04:00
* @memberof TaskQueue
2018-08-31 14:16:20 -04:00
*/
_spend (cost) {
if (cost <= this._tokenCount) {
this._tokenCount -= cost;
return true;
}
return false;
}
/**
* Create a Promise which will resolve when the bucket will be able to "afford" the given cost.
* Note that this won't refill the bucket, so make sure to refill after the promise resolves.
*
* @param {number} cost - wait until the token count is at least this much.
* @returns {Promise} - to be resolved once the bucket is due for a token count greater than or equal to the cost.
2018-09-14 22:34:34 -04:00
* @memberof TaskQueue
2018-08-31 14:16:20 -04:00
*/
_waitUntilAffordable (cost) {
if (cost <= this._tokenCount) {
return Promise.resolve();
}
2018-08-31 15:38:06 -04:00
if (!(cost <= this._maxTokens)) {
return Promise.reject(new Error(`Task cost ${cost} is greater than bucket limit ${this._maxTokens}`));
2018-08-31 14:16:20 -04:00
}
return new Promise(resolve => {
const tokensNeeded = Math.max(cost - this._tokenCount, 0);
2018-08-31 14:16:20 -04:00
const estimatedWait = Math.ceil(1000 * tokensNeeded / this._refillRate);
let timeout = null;
const onTimeout = () => {
if (this._timeout === timeout) {
this._timeout = null;
}
resolve();
};
2019-02-07 19:31:36 -05:00
this._timeout = timeout = this._timer.setTimeout(onTimeout, estimatedWait);
2018-08-31 14:16:20 -04:00
});
}
}
2018-09-14 22:34:34 -04:00
module.exports = TaskQueue;