Skip to content

Commit

Permalink
more work
Browse files Browse the repository at this point in the history
  • Loading branch information
Uzlopak committed Aug 23, 2024
1 parent ea4e9e8 commit cf4ca88
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 173 deletions.
268 changes: 198 additions & 70 deletions lib/util/timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,98 +12,167 @@
*/

/**
* TICK_MS is the desired time in milliseconds between each tick. Target
* is 499ms to be just under 500ms
* The RESOLUTION_MS is the desired time in milliseconds.
*
* @type {number}
* @default 1000
*/
const TICK_MS = 499
const RESOLUTION_MS = 1e3

/**
* fastNow is containing the time since starting the process (so called uptime)
* in milliseconds.
* TICK_MS is the desired time in milliseconds between each tick. Target
* is the half of the resolution time minus 1 ms to account for a potential
* overhead of the event loop.
*
* @type {number}
* @default 499
*/
let fastNow = performance.now()
const TICK_MS = (RESOLUTION_MS >> 1) - 1

/**
* The fastNowTimeout is the central Node.js timer that will be used to manage
* the fast timers.
* The fastNowTimeout is one Node.js timer that will be used to manage
* the FastTimers.
*
* @type {NodeJS.Timeout|null}
*/
let fastNowTimeout

/**
* The kFastTimeout symbol is used to identify the FastTimeout instances.
* The kFastTimer symbol is used to identify the FastTimer instances.
* @type {Symbol}
*/
const kFastTimeout = Symbol('kFastTimeout')
const kFastTimer = Symbol('kFastTimer')

const nativeSetTimeout = global.setTimeout
const nativeClearTimeout = global.clearTimeout

/**
* The FastTimeouts array contains all the active timers that are being managed
* by the fast timer implementation.
* The FastTimers array contains all the active FastTimers.
*
* @type {FastTimeout[]}
* @type {FastTimer[]}
*/
const FastTimers = []

/**
* The following constants are used to represent the state of a FastTimer.
*/
const FastTimeouts = []

/**
* The NOT_IN_LIST constant is used to mark the FastTimer as not in the
* FastTimers array. FastTimers with this status will not be processed in the
* next tick by the onTick function.
*
* @type {-2}
*/
const NOT_IN_LIST = -2

/**
* The TO_BE_CLEARED constant is used to mark the FastTimer as to be planned to
* be removed from the FastTimers array. FastTimers with this status will be
* removed from the FastTimers array in the next tick by the onTick function and
* not be processed again.
*
* A FastTimer can be in the TO_BE_CLEARED state if the clear method is called
* on the FastTimer instance.
*
* @type {-1}
*/
const TO_BE_CLEARED = -1

/**
* The PENDING constant is used to mark the FastTimer as waiting for the next
* tick to be processed by the onTick function. FastTimers with this status will
* have their _idleStart value set to the fastNow value and set to ACTIVE in the
* next tick by the onTick function.
*
* @type {0}
*/
const PENDING = 0

/**
* The ACTIVE constant is used to mark the FastTimer as active and waiting for
* the timer to expire. FastTimers with this status will be checked in the next
* tick by the onTick function if the timer has expired and thus triggering the
* callback.
*
* @type {1}
*/
const ACTIVE = 1

/**
* The onTick function is called every TICK_MS milliseconds and is responsible
* for processing the FastTimers array.
*/
function onTick () {
// Refresh the fastNow value to the current uptime of the process
fastNow = performance.now()
/**
* The fastNow variable is used to store the current time in milliseconds
* since the process started.
*
* @type {number}
*/
const fastNow = performance.now()

/**
* The idx variable is used to iterate over the FastTimeouts array.
* Expired FastTimeouts will be removed from the array by being
* The idx variable is used to iterate over the FastTimers array.
* Expired FastTimers will be removed from the array by being
* replaced with the last element in the array. Thus, the idx variable
* will only be incremented if the current element is not removed.
*
* @type {number}
*/
let idx = 0

/**
* The len variable will contain the length of the FastTimeouts array
* and will be decremented when a FastTimeout is removed from the array.
* The len variable will contain the length of the FastTimers array
* and will be decremented when a FastTimer is removed from the array.
*
* @type {number}
*/
let len = FastTimeouts.length
let len = FastTimers.length

while (idx < len) {
/**
* @type {FastTimeout}
* @type {FastTimer}
*/
const timer = FastTimeouts[idx]
const timer = FastTimers[idx]

if (timer.state === PENDING) {
timer.state = fastNow + timer.delay - TICK_MS
} else if (timer.state > 0 && fastNow >= timer.state) {
timer.state = TO_BE_CLEARED
timer.callback(timer.opaque)
// If the timer is in the PENDING state, set the _idleStart value to the
// fastNow value and set the state to ACTIVE.
// If the timer is in the ACTIVE state and the timer has expired, it will
// be processed in the next tick.
if (timer._state === PENDING) {
timer._idleStart = fastNow - TICK_MS
timer._state = ACTIVE
} else if (
timer._state === ACTIVE &&
fastNow >= timer._idleStart + timer._idleTimeout
) {
timer._state = TO_BE_CLEARED
timer._onTimeout(timer._timerArg)
}

if (timer.state === TO_BE_CLEARED) {
timer.state = NOT_IN_LIST
if (timer._state === TO_BE_CLEARED) {
timer._state = NOT_IN_LIST

// Remove the timer from the list of active timers if is the last one
if (idx === len - 1) {
FastTimeouts.pop()
FastTimers.pop()
} else {
// Otherwise, move the last timer with the current timer
FastTimeouts[idx] = FastTimeouts.pop()
FastTimers[idx] = FastTimers.pop()
}
len -= 1
} else {
idx += 1
}
}

if (FastTimeouts.length !== 0) {
// If there are no active timers, clear the fastNowTimeout. It will be
// refreshed if a new FastTimer is instantiated.
if (FastTimers.length === 0) {
nativeClearTimeout(fastNowTimeout)
// Otherwise, refresh the fastNowTimeout for the next tick.
} else {
refreshTimeout()
}
}
Expand All @@ -121,55 +190,107 @@ function refreshTimeout () {
}

/**
* The FastTimeout class is a class that represents a timer that is managed by
* The FastTimer class is a class that represents a timer that is managed by
* the fast timer implementation.
*/
class FastTimeout {
[kFastTimeout] = true
class FastTimer {
[kFastTimer] = true

/**
* If the state of the timer is a non-negative number, it represents the
* time in milliseconds when the timer should expire. Values equal or less
* than zero represent the following states:
* - NOT_IN_LIST: The timer is not in the list of active timers.
* - TO_BE_CLEARED: The timer is in the list of active timers but is marked
* to be cleared.
* to be cleared and removed from the FastTimers array in the next tick.
* - PENDING: The timer is in the list of active timers and is waiting for
* the next tick to be activated.
* - ACTIVE: The timer is in the list of active timers and is waiting for
* the timer to expire.
* @type {number}
* @private
*/
_state = NOT_IN_LIST

/**
* The time in milliseconds when the timer should expire.
*
* @type {number}
* @private
*/
_idleTimeout = -1

/**
* The time in milliseconds when the timer was started. This value is used to
* calculate when the timer should expire. If the timer is in the PENDING
* state, this value is set to the fastNow value in the next tick by the
* onTick function.
*
* @type {number}
* @private
*/
state = NOT_IN_LIST
_idleStart = -1

/**
* The function to be executed when the timer expires.
* @type {Function}
*/
_onTimeout

/**
* The argument to be passed to the function when the timer expires.
*/
_timerArg

/**
* @constructor
* @param {Function} callback A function to be executed after the timer expires.
* @param {number} delay The time, in milliseconds that the timer should wait before the specified function or code is executed.
* @param {*} opaque
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should wait
* before the specified function or code is executed.
* @param {*} arg
*/
constructor (callback, delay, opaque) {
this.callback = callback
this.delay = delay
this.opaque = opaque
constructor (callback, delay, arg) {
this._onTimeout = callback
this._idleTimeout = delay
this._timerArg = arg

this.refresh()
}

/**
* Sets the timer's start time to the current time, and reschedules the timer
* to call its callback at the previously specified duration adjusted to the
* current time.
* Using this on a timer that has already called its callback will reactivate
* the timer.
*/
refresh () {
if (this.state === NOT_IN_LIST) {
FastTimeouts.push(this)
if (!fastNowTimeout || FastTimeouts.length === 1) {
refreshTimeout()
}
// In the special case that the timer is not in the list of active timers,
// add it back to the array to be processed in the next tick by the onTick
// function.
if (this._state === NOT_IN_LIST) {
FastTimers.push(this)
}

// If the timer is the only active timer, refresh the fastNowTimeout for
// better resolution.
if (!fastNowTimeout || FastTimers.length === 1) {
refreshTimeout()
}

this.state = PENDING
// Setting the state to PENDING will cause the timer to be reset in the
// next tick by the onTick function.
this._state = PENDING
}

/**
* The clear method marks the timer as to be cleared. The timer will be
* removed from the list of active timers in the next tick.
* The clear method marks the timer as to be cleared, by setting the _state
* properly. The timer will be removed from the list of active timers in the
* next tick by the onTick function
*/
clear () {
this.state = TO_BE_CLEARED
this._state = TO_BE_CLEARED
}
}

Expand All @@ -182,28 +303,35 @@ class FastTimeout {
*/
module.exports = {
/**
* The setTimeout() method sets a timer which executes a function once the timer expires.
* @param {Function} callback A function to be executed after the timer expires.
* @param {number} delay The time, in milliseconds that the timer should wait before the specified function or code is executed.
* @param {*} opaque
* @returns
* The setTimeout() method sets a timer which executes a function once the
* timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {NodeJS.Timeout|FastTimer}
*/
setTimeout (callback, delay, opaque) {
// If the delay is less than or equal to 1 second, use the native setTimeout function.
return delay <= 1e3
? nativeSetTimeout(callback, delay, opaque)
: new FastTimeout(callback, delay, opaque)
setTimeout (callback, delay, arg) {
// If the delay is less than or equal to the RESOLUTION_MS value return a
// native Node.js Timer instance.
return delay <= RESOLUTION_MS
? nativeSetTimeout(callback, delay, arg)
: new FastTimer(callback, delay, arg)
},
/**
* The clearTimeout() method cancels a timeout previously established by calling setTimeout().#
* The clearTimeout method cancels an instantiated Timer previously created
* by calling setTimeout.
*
* @param {NodeJS.Timeout|FastTimeout} timeout
* @param {NodeJS.Timeout|FastTimer} timeout
*/
clearTimeout (timeout) {
// If the timeout is an instance of Timeout, call the clear method.
if (timeout[kFastTimeout]) {
// If the timeout is a FastTimer, call its own clear method.
if (timeout[kFastTimer]) {
timeout.clear()
// Otherwise, call the Node.js native clearTimeout function.
// Otherwise it an instance of a native NodeJS.Timeout, so call the
// Node.js native clearTimeout function.
} else {
nativeClearTimeout(timeout)
}
Expand All @@ -213,5 +341,5 @@ module.exports = {
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
kFastTimeout
kFastTimer
}
Loading

0 comments on commit cf4ca88

Please sign in to comment.