Benchmarking and testing timer implementations https://rentry.co/3o4kgugy
Hover over a code block and click the copy button at the top right of it.
Tester
This can be run anywhere e.g. browser console
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 | const api = { broadcastMessage: console.log }
/**
* TIMER IMPLEMENTATION
* *************************************************************************************************** */
TTimers = {}
nextId = 1
tickCount = 0
head = 0
tail = 0
minTargetTick = Infinity
timersAddedMidProc = null
timers = TTimers
{
const error = msg => api.broadcastMessage(msg, { color: "#ff9d87" })
setTickTimeout = (fn, delay = 0) => {
const id = nextId++
const targetTick = tickCount + delay
timers[id] = [fn, targetTick, tail, 0]
if (tail)
timers[tail][3] = id
else
head = id
tail = id
if (targetTick < minTargetTick)
minTargetTick = targetTick
timersAddedMidProc = true
if (!delay) {
try { fn() }
catch (e) { error("TTI: " + e) }
clearTickTimeout(id)
}
return id
}
clearTickInterval = clearTickTimeout = id => {
const timer = timers[id]
if (timer) {
const prev = timer[2]
const next = timer[3]
if (prev)
timers[prev][3] = next
else
head = next
if (next)
timers[next][2] = prev
else
tail = prev
delete timers[id]
}
}
setTickInterval = (fn, delay) => {
const id = setTickTimeout(fn, delay ||= 1)
timers[id][4] = delay
return id
}
tick = (fifty, noTick) => {
if (noTick || ++tickCount >= minTargetTick) {
timersAddedMidProc = false
let newMinTick = Infinity
let id = head
while (id) {
const timer = timers[id]
if (timer[1] <= tickCount) {
try { timer[0]() }
catch (e) { error("TT: " + e) }
if (timer[4]) {
const newTargetTick = tickCount + timer[4]
timer[1] = newTargetTick
if (newTargetTick < newMinTick)
newMinTick = newTargetTick
} else {
clearTickTimeout(id)
}
} else if (timer[1] < newMinTick) {
newMinTick = timer[1]
}
id = timer[3]
}
minTargetTick = newMinTick
if (timersAddedMidProc)
tick(1, true)
}
}
}
/**
* ADAPTER - CHANGE EACH FUNCTION TO SUPPORT THE IMPL. MAKE SURE ALL VARIABLES ARE RESET CORRECTLY
* *************************************************************************************************** */
function resetTimerSystem() {
TTimers = {}
nextId = 1
tickCount = 0
head = 0
tail = 0
minTargetTick = Infinity
timersAddedMidProc = null
timers = TTimers
}
function getPendingCount() {
return Object.keys(TTimers).length
}
function setTimeoutWRP(fn, delay) {
return setTickTimeout(fn, delay)
}
function setIntervalWRP(fn, delay) {
return setTickInterval(fn, delay)
}
function clearTimeoutWRP(theReturnedValue) {
return clearTickTimeout(theReturnedValue)
}
function clearIntervalWRP(theReturnedValue) {
clearTimeoutWRP(theReturnedValue)
}
/** TESTING SYSTEM
* *************************************************************************************************** */
const tests = []
function addTest(name, fn) {
tests.push({ name, fn })
}
function runTicks(count, wait = 50) {
return new Promise(resolve => {
let ticksRun = 0
function next() {
if (ticksRun < count) {
try { tick() } catch (e) { console.error(e) }
ticksRun++
setTimeout(next, wait)
} else {
setTimeout(resolve, wait)
}
}
next()
})
}
function assert(condition, msg) {
if (!condition) throw new Error(msg)
}
async function runTests() {
let passed = 0, failed = 0
console.log("\n\n")
for (const test of tests) {
try {
resetTimerSystem()
console.info(`⬜ TESTING ${test.name}\n`)
await test.fn()
++passed
} catch (error) {
console.error(`🟥 FAILED: ${error.message}\n`)
++failed
}
}
console.log(`\nDONE. Total: ${tests.length} - Passed: ${passed} - Failed: ${failed}`)
}
/** TESTS
* *********************************************************************************************** */
addTest("Callback clears itself and schedules same function again", async () => {
let runCount = 0
let id = null
const cb = () => {
++runCount
clearTimeoutWRP(id)
if (runCount < 2) id = setTimeoutWRP(cb, 1)
}
id = setTimeoutWRP(cb, 1)
await runTicks(3)
assert(runCount == 2, `Expected callback to run twice, got ${runCount}`)
})
addTest("runTicks(0) should not trigger any timers", async () => {
let executed = false
setTimeoutWRP(() => { executed = true }, 1)
await runTicks(0)
assert(!executed, "Callback should not execute on runTicks(0)")
await runTicks(1)
assert(executed, "Callback should execute after 1 tick")
})
addTest("Immediate clear after scheduling should prevent execution", async () => {
let ran = false
const id = setTimeoutWRP(() => { ran = true }, 1)
clearTimeoutWRP(id)
await runTicks(1)
assert(!ran, "Callback executed despite being cleared immediately")
})
addTest("Reschedule timer from inside running callback", async () => {
let ran = false
setTimeoutWRP(() => {
setTimeoutWRP(() => { ran = true }, 1)
}, 1)
await runTicks(2)
assert(ran, "Inner rescheduled timer did not run")
})
addTest("Cancel timer before it runs", async () => {
let called = false
let id = setTimeoutWRP(() => { called = true }, 3)
await runTicks(1)
clearTimeoutWRP(id)
await runTicks(5)
assert(!called, "Cancelled timer still ran")
})
addTest("Clearing same timer multiple times", async () => {
let id = setTimeoutWRP(() => { throw new Error("Should not run") }, 3)
clearTimeoutWRP(id)
clearTimeoutWRP(id)
await runTicks(4)
})
addTest("Nested setInterval scheduling", async () => {
let count = 0
setIntervalWRP(() => {
++count
if (count == 1) setIntervalWRP(() => { count++ }, 2)
}, 1)
await runTicks(6)
assert(count >= 3, `Expected at least 3 executions, got ${count}`)
})
addTest("Timer clears itself before end", async () => {
let ran = false
let id = setTimeoutWRP(() => {
ran = true
clearTimeoutWRP(id)
}, 1)
await runTicks(2)
assert(ran, "Callback did not run")
})
addTest("Execution order for same-delay timers", async () => {
let result = []
setTimeoutWRP(() => result.push("a"), 2)
setTimeoutWRP(() => result.push("b"), 2)
setTimeoutWRP(() => result.push("c"), 2)
await runTicks(2)
assert(result.join("") == "abc", `Expected "abc", got "${result.join("")}"`)
})
addTest("Interval clears itself inside callback before reschedule", async () => {
let count = 0
let id = setIntervalWRP(() => {
++count
clearIntervalWRP(id)
}, 1)
await runTicks(5)
assert(count == 1, `Expected 1 execution, got ${count}`)
})
addTest("Interval A clears Interval B mid-run", async () => {
let a = 0, b = 0
let idB = setIntervalWRP(() => { b++ }, 1)
let idA = setIntervalWRP(() => {
++a
clearIntervalWRP(idB)
}, 2)
await runTicks(5)
assert(a >= 2, `Expected at least 2 executions of A, got ${a}`)
assert(b <= 2, `Expected B to stop early, got ${b}`)
})
addTest("Callback reschedules itself manually", async () => {
let count = 0
const fn = () => {
++count
if (count < 3) setTimeoutWRP(fn, 1)
}
setTimeoutWRP(fn, 1)
await runTicks(6)
assert(count == 3, `Expected 3 executions, got ${count}`)
})
addTest("Clearing already-run timer is safe", async () => {
let ran = false
const id = setTimeoutWRP(() => { ran = true }, 1)
await runTicks(1)
assert(ran, "Callback should have run")
clearTimeoutWRP(id)
})
addTest("Immediate timer scheduled inside another callback", async () => {
let order = []
setTimeoutWRP(() => {
order.push("outer")
setTimeoutWRP(() => order.push("inner"), 0)
}, 0)
await runTicks(2)
assert(order.join(",") == "outer,inner", `Expected outer,inner, got ${order}`)
})
addTest("Repeating timer that throws", async () => {
let ran = 0
setIntervalWRP(() => {
++ran
throw new Error("oops")
}, 1)
await runTicks(3)
assert(ran == 3, `Expected 3 executions despite errors, got ${ran}`)
})
addTest("Stress: 20 timers, clear half", async () => {
let ran = 0
const ids = []
for (let i = 0; i < 20; i++) {
ids.push(setTimeoutWRP(() => { ran++ }, 5))
}
for (let i = 0; i < 10; i++) {
clearTimeoutWRP(ids[i])
}
await runTicks(6)
assert(ran == 10, `Expected 10 executions, got ${ran}`)
})
addTest("setTimeout basic functionality", async () => {
let executed = false
setTimeoutWRP(() => { executed = true }, 1)
assert(getPendingCount() == 1, `Expected count 1, got ${getPendingCount()}`)
await runTicks(1)
assert(executed, "Callback was not executed")
assert(getPendingCount() == 0, `Expected count 0, got ${getPendingCount()}`)
})
addTest("setTimeout with delay", async () => {
let executed = false
setTimeoutWRP(() => { executed = true }, 3)
await runTicks(2)
assert(!executed, "Callback executed too early")
await runTicks(1)
assert(executed, "Callback was not executed after delay")
})
addTest("setInterval basic functionality", async () => {
let count = 0
const id = setIntervalWRP(() => { count++ }, 2)
await runTicks(6)
assert(count == 3, `Expected 3 executions, got ${count}`)
clearIntervalWRP(id)
})
addTest("clearTimeout before execution", async () => {
let executed = false
const id = setTimeoutWRP(() => { executed = true }, 3)
await runTicks(1)
clearTimeoutWRP(id)
await runTicks(3)
assert(!executed, "Callback executed after being cleared")
assert(getPendingCount() == 0, `Expected count 0, got ${getPendingCount()}`)
})
addTest("clearInterval after some executions", async () => {
let count = 0
const id = setIntervalWRP(() => { count++ }, 1)
await runTicks(3)
clearIntervalWRP(id)
await runTicks(3)
assert(count == 3, `Expected 3 executions, got ${count}`)
})
addTest("setTimeout with zero delay", async () => {
let executed = false
setTimeoutWRP(() => { executed = true }, 1)
assert(!executed, "Callback executed synchronously")
await runTicks(1)
assert(executed, "Callback not executed on next tick")
})
addTest("Error in callback", async () => {
let errorCaught = false
setTimeoutWRP(() => {
try {
throw new Error("Test error")
} catch (e) {
errorCaught = true
}
}, 1)
await runTicks(1)
assert(errorCaught, "Error should have been thrown")
})
addTest("Creating and clearing timeouts in various orders", async () => {
let results = []
const id1 = setTimeoutWRP(() => { results.push(1) }, 1)
const id2 = setTimeoutWRP(() => { results.push(2) }, 2)
const id3 = setTimeoutWRP(() => { results.push(3) }, 3)
const id4 = setTimeoutWRP(() => { results.push(4) }, 4)
clearTimeoutWRP(id2)
await runTicks(2)
clearTimeoutWRP(id4)
await runTicks(2)
assert(results.length == 2 && results[0] == 1 && results[1] == 3, `Expected [1,3], got [${results}]`)
})
addTest("Callback creating new timeouts", async () => {
let count = 0
setTimeoutWRP(() => {
++count
if (count < 3) setTimeoutWRP(() => { count++ }, 1)
}, 1)
await runTicks(6)
assert(count >= 2, `Expected at least 2 count, got ${count}`)
})
addTest("Callback clearing other timeouts", async () => {
let results = []
const id2 = setTimeoutWRP(() => { results.push(2) }, 2)
const id1 = setTimeoutWRP(() => {
results.push(1)
clearTimeoutWRP(id2)
}, 1)
await runTicks(3)
assert(results.length == 1 && results[0] == 1, `Expected [1], got [${results}]`)
})
addTest("Many simultaneous timeouts with same delay", async () => {
let executed = 0
for (let i = 0; i < 10; i++) {
setTimeoutWRP(() => { executed++ }, 3)
}
await runTicks(4)
assert(executed == 10, `Expected 10 executions, got ${executed}`)
})
addTest("Many timeouts with various delays", async () => {
let results = []
for (let i = 1; i <= 5; i++) {
setTimeoutWRP(() => { results.push(i) }, i)
}
await runTicks(6)
assert(results.join(",") == "1,2,3,4,5", `Expected ordered results, got [${results}]`)
})
addTest("Self-clearing interval", async () => {
let count = 0
let intervalId = setIntervalWRP(() => {
++count
if (count >= 3) clearIntervalWRP(intervalId)
}, 1)
await runTicks(10)
assert(count >= 3, `Expected at least 3 executions, got ${count}`)
const finalCount = count
await runTicks(3)
assert(count == finalCount, `Interval continued after clearing`)
})
addTest("ID reuse after many operations", async () => {
const ids = []
for (let i = 0; i < 50; i++) {
ids.push(setTimeoutWRP(() => { }, 100))
}
for (let i = 0; i < 25; i++) {
clearTimeoutWRP(ids[i])
}
setTimeoutWRP(() => { }, 100)
})
// start tests
setTimeout(runTests, 100)
|
Benchmarker
- you have to manually manage the global names unfortunately
- it can be run anywhere but quickjs-emscripten has performance differences as in bloxd scripting
- the timer implementation has to use IDs, starting from 1 (due to how clearing works)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 | api = { now: Date.now }
globalThis.log ??= console.log
const BENCH_AMOUNT = 500000 // amount of set & clear
const TICK_ITERATIONS = 100000 // number of ticks to simulate
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
const benchDelays = new Array(BENCH_AMOUNT).fill().map(() => randomInt(1, TICK_ITERATIONS))
const CLEAR_PROB = 0.5
function generateClearSchedule(timerCount, iterations, maxDelay) {
const totalClears = Math.floor(timerCount * CLEAR_PROB)
const clearBatchSize = Math.max(1, Math.floor(timerCount * CLEAR_PROB / (iterations * maxDelay / timerCount)))
const schedule = {}
// ids = 1 to timerCount
const availableIds = [...Array(timerCount + 1).keys()]
availableIds.shift()
let remainingClears = totalClears
let currentTick = 0
while (remainingClears > 0 && availableIds.length > 0) {
const numToClear = Math.min(clearBatchSize, remainingClears, availableIds.length)
schedule[currentTick] = []
for (let i = 0; i < numToClear; i++) {
const randomIndex = randomInt(0, availableIds.length - 1)
schedule[currentTick].push(availableIds[randomIndex])
availableIds.splice(randomIndex, 1)
}
remainingClears -= numToClear
currentTick += Math.max(1, Math.floor(iterations / totalClears))
}
return schedule
}
// tickTimers
var tickTimers; function setTickTimeout(e, t = 1, r) { let i = [e, api.now() + t * 50, t, r]; return (tickTimers ??= new Set).add(i), i } function setTickInterval(e, t) { return setTickTimeout(e, t, !0) } function clearTickTimeout(e) { tickTimers?.delete(e) } function clearTickInterval(e) { clearTickTimeout(e) } tickOld = () => { if (tickTimers?.size) { let e = api.now(); for (let t of tickTimers) { let [r, i, c, n] = t; if (e >= i) { try { r() } catch (o) { api.broadcastMessage("Tick timer: " + o, { color: "#ff9d87" }) } n ? t[1] = e + c * 50 : tickTimers.delete(t) } } } }
// TimerQueue
class MinQueue { constructor(i, t, e) { t = e = Uint32Array, this.c = i, this.k = new t(i + 1), this.p = new e(i + 1), this.h = !1, this.l = 0 } bbu(i) { const t = this.k[i], e = this.p[i]; for (; i > 1;) { const t = i >>> 1; if (this.p[t] <= e) break; this.k[i] = this.k[t], this.p[i] = this.p[t], i = t } this.k[i] = t, this.p[i] = e } bbd(i) { const t = this.k[i], e = this.p[i], s = 1 + (this.l >>> 1), r = this.l + 1; for (; i < s;) { const t = i << 1; let s = this.p[t], h = this.k[t], n = t; const u = t + 1; if (u < r) { const i = this.p[u]; i < s && (s = i, h = this.k[u], n = u) } if (s >= e) break; this.k[i] = h, this.p[i] = s, i = n } this.k[i] = t, this.p[i] = e } push(i, t) { if (this.l === this.c) throw "heap full"; if (this.h) this.k[1] = i, this.p[1] = t, this.l++, this.bbd(1), this.h = !1; else { const e = this.l + 1; this.k[e] = i, this.p[e] = t, this.l++, this.bbu(e) } } pop() { if (0 !== this.l) return this.rpe(), this.l--, this.h = !0, this.k[1] } peekPriority() { if (0 !== this.l) return this.rpe(), this.p[1] } peek() { if (0 !== this.l) return this.rpe(), this.k[1] } rpe() { this.h && (this.k[1] = this.k[this.l + 1], this.p[1] = this.p[this.l + 1], this.bbd(1), this.h = !1) } }; currently_running_timer = null, TimerQueue = new MinQueue(BENCH_AMOUNT), TimerDictionary = new Array(BENCH_AMOUNT), TimerNum = 1, TickNum = 0; function setTimeout(i, t) { if ("function" != typeof i) return; let e = TickNum + t; return TimerDictionary[TimerNum] = i, TimerQueue.push(TimerNum, e), TimerNum++ } function clearTimeout(i) { delete TimerDictionary[i] } function tickQueue() { for (TickNum++; ;)if (currently_running_timer) currently_running_timer(), currently_running_timer = void 0; else { if (!(TimerQueue.peekPriority() <= TickNum)) break; { let i = TimerQueue.peek(); currently_running_timer = TimerDictionary[i], TimerQueue.pop(), delete TimerDictionary[i] } } }
// SulfroxNew
sulfNewTickNum = 0, IterationNum = 0, sub_iteration_num = 0, Timer_Delay_Dictionary = new Array(BENCH_AMOUNT); function sulfNewTick() { for (++sulfNewTickNum; IterationNum <= sulfNewTickNum; sub_iteration_num = 0, ++IterationNum) { if (!(IterationNum in Timer_Delay_Dictionary)) continue; let i = Timer_Delay_Dictionary[IterationNum], e = i.length; for (; sub_iteration_num < e; ++sub_iteration_num)i[sub_iteration_num](); delete Timer_Delay_Dictionary[IterationNum] } } function sulfNewSetTimeout(i, e) { if (typeof i != "function") throw new TypeError("not a function"); let t = sulfNewTickNum + e, n; t in Timer_Delay_Dictionary ? n = Timer_Delay_Dictionary[t] : n = Timer_Delay_Dictionary[t] = [], n.push(i) }
// new tickTimers L
TTimers = {}
nextId = 1
tickCount = 0
head = 0
tail = 0
minTargetTick = Infinity
timersAddedMidProc = null
{
const error = msg => api.broadcastMessage(msg, { color: "#ff9d87" })
const timers = TTimers
setTickTimeout = (fn, delay = 0) => {
const id = nextId++
const targetTick = tickCount + delay
timers[id] = [fn, targetTick, tail, 0]
if (tail)
timers[tail][3] = id
else
head = id
tail = id
if (targetTick < minTargetTick)
minTargetTick = targetTick
timersAddedMidProc = true
if (!delay) {
try { fn() } catch (e) { error("TTI: " + e) }
clearTickTimeout(id)
}
return id
}
clearTickInterval = clearTickTimeout = id => {
const timer = timers[id]
if (timer) {
const prev = timer[2]
const next = timer[3]
if (prev)
timers[prev][3] = next
else
head = next
if (next)
timers[next][2] = prev
else
tail = prev
delete timers[id]
}
}
setTickInterval = (fn, delay) => {
const id = setTickTimeout(fn, delay ||= 1)
timers[id][4] = delay
return id
}
tick = (fifty, noTick) => {
if (noTick || ++tickCount >= minTargetTick) {
timersAddedMidProc = false
let newMinTick = Infinity
let id = head
while (id) {
const timer = timers[id]
if (timer[1] <= tickCount) {
try { timer[0]() } catch (e) {
if (e.message != "interrupted")
error("TT: " + e)
}
if (timer[4]) {
const newTargetTick = tickCount + timer[4]
timer[1] = newTargetTick
if (newTargetTick < newMinTick)
newMinTick = newTargetTick
} else {
clearTickTimeout(id)
}
} else if (timer[1] < newMinTick) {
newMinTick = timer[1]
}
id = timer[3]
}
minTargetTick = newMinTick
if (timersAddedMidProc)
tick(1, true)
}
}
}
// new tickTimers H
TTimersX = {}
tickToIdsX = {}
nextIdX = 1
tickCountX = 0
processingTickX = 1
processingTimerIndexX = 0
interruptedTickCountX = null
{
const error = msg => api.broadcastMessage(msg, { color: "#ff9d87" })
const timers = TTimersX
setTickTimeoutX = (fn, delay = 0) => {
const id = nextIdX++
const targetTick = tickCountX + delay
timers[id] = [fn, targetTick]
const tArr = tickToIdsX[targetTick] ??= []
tArr[tArr.length] = id
if (!delay) {
try { fn() } catch (e) { error("TTI: " + e) }
clearTickTimeoutX(id)
}
return id
}
setTickIntervalX = (fn, delay) => {
const id = setTickTimeoutX(fn, delay ||= 1)
timers[id][2] = delay
return id
}
clearTickIntervalX = clearTickTimeoutX = id => delete timers[id]
tickX = () => {
const ids = tickToIdsX[
tickCountX = interruptedTickCountX ?? tickCountX + 1
]
if (ids) for (; processingTimerIndexX < ids.length; ++processingTimerIndexX) {
const id = ids[processingTimerIndexX]
const timer = timers[id]
if (timer == null) continue
interruptedTickCountX = tickCountX
try { timer[0]() } catch (e) {
if (e.message != "interrupted")
error("TT: " + e)
}
interruptedTickCountX = null
if (timer[2]) {
const tArr = tickToIdsX[
timer[1] = tickCountX + timer[2]
] ??= []
tArr[tArr.length] = id
} else {
delete timers[id]
}
}
delete tickToIdsX[tickCountX]
++processingTickX
processingTimerIndexX = 0
}
}
function reset() {
tickTimers = new Set()
TimerQueue = new MinQueue(BENCH_AMOUNT)
TimerDictionary = []
TimerNum = 1
TickNum = 0
currently_running_timer = null
TTimers = {}
nextId = 1
tickCount = 0
head = 0
tail = 0
minTargetTick = Infinity
timersAddedMidProc = null
TTimersX = {}
tickToIdsX = {}
nextIdX = 1
tickCountX = 0
processingTickX = 1
processingTimerIndexX = 0
interruptedTickCountX = null
sulfNewTickNum = 0
IterationNum = 0
sub_iteration_num = 0
Timer_Delay_Dictionary = {}
}
function benchInsert(fn, delays, name) {
const start = api.now()
const ids = []
for (let i = 0; i < BENCH_AMOUNT; i++)
ids.push(fn(() => { }, delays[i]))
log(`${name} insertion: ${api.now() - start}ms`)
return ids
}
function benchClear(fn, ids, name) {
const start = api.now()
for (const id of ids)
fn(id)
log(`${name} clearing: ${api.now() - start}ms`)
}
function benchTick(setFn, clearFn, tickFn, delays, delayRange, name, clearSchedule) {
reset()
let callbackCount = 0
let totalTime = 0
let tickCounter = 0
for (const d of delays)
setFn(() => { ++callbackCount }, d)
for (let i = 0; i < TICK_ITERATIONS; i++) {
const start = api.now()
tickFn()
totalTime += api.now() - start
tickCounter++
if (clearSchedule?.[i] && clearFn) {
for (const id of clearSchedule[i]) {
clearFn(id)
}
}
}
log(`${name} (${delays.length} timers, ${delayRange[0]}-${delayRange[1]} tick delay, ${TICK_ITERATIONS} ticks): ${totalTime}ms (avg ${totalTime / tickCounter}ms/tick) - callbacks run: ${callbackCount}`)
}
function start() {
log(`\n--- Set ${BENCH_AMOUNT} timers ---`)
// const tickTimerIdsS = benchSetTimeout(sulfNewSetTimeout, benchDelays, "SulfroxNew")
let tickTimerIdsQ = benchInsert(setTimeout, benchDelays, " TimerQueue")
let tickTimerIdsX = benchInsert(setTickTimeoutX, benchDelays, " tickTimersH")
let tickTimerIds = benchInsert(setTickTimeout, benchDelays, " tickTimersL")
log(`\n--- Set/Clear ${BENCH_AMOUNT} timers ---`)
// log("- no clear func -")
benchClear(clearTimeout, tickTimerIdsQ, " TimerQueue")
benchClear(clearTickTimeoutX, tickTimerIdsX, " tickTimersH")
benchClear(clearTickTimeout, tickTimerIds, " tickTimersL")
reset()
tickTimerIdsQ = benchInsert(setTimeout, benchDelays, " TimerQueue")
tickTimerIdsX = benchInsert(setTickTimeoutX, benchDelays, " tickTimersH")
tickTimerIds = benchInsert(setTickTimeout, benchDelays, " tickTimersL")
benchClear(clearTimeout, tickTimerIdsQ, " TimerQueue")
benchClear(clearTickTimeoutX, tickTimerIdsX, " tickTimersH")
benchClear(clearTickTimeout, tickTimerIds, " tickTimersL")
const SHORT_RANGE = [1, 60]
const LONG_RANGE = [1200, 1200 * 60]
log("\n--- Tick ---")
const scenarios = [
{ count: 10, delays: SHORT_RANGE },
{ count: 10, delays: LONG_RANGE },
{ count: 10, delays: LONG_RANGE, mixed: true },
{ count: 200, delays: SHORT_RANGE },
{ count: 200, delays: LONG_RANGE },
{ count: 200, delays: LONG_RANGE, mixed: true },
{ count: 300, delays: [1, 20] },
{ count: 300, delays: SHORT_RANGE },
{ count: 300, delays: [1, 1200] },
{ count: 300, delays: LONG_RANGE },
{ count: 300, delays: SHORT_RANGE, mixed: true },
{ count: 300, delays: LONG_RANGE, mixed: true },
{ count: 10000, delays: SHORT_RANGE },
{ count: 10000, delays: SHORT_RANGE, mixed: true },
{ count: 10000, delays: LONG_RANGE },
{ count: 10000, delays: LONG_RANGE, mixed: true },
{ count: 20000, delays: SHORT_RANGE },
{ count: 20000, delays: SHORT_RANGE, mixed: true },
{ count: 20000, delays: LONG_RANGE },
{ count: 20000, delays: LONG_RANGE, mixed: true }
]
for (const scenario of scenarios) {
const delays = new Array(scenario.count).fill().map(() => randomInt(scenario.delays[0], scenario.delays[1]))
const clearSchedule = scenario.mixed ? generateClearSchedule(scenario.count, TICK_ITERATIONS, Math.max(...scenario.delays)) : null
benchTick(
setTimeout,
clearTimeout,
tickQueue,
delays,
[scenario.delays[0], scenario.delays[1]],
" TimerQueue",
clearSchedule
)
benchTick(
setTickTimeoutX,
clearTickTimeoutX,
tickX,
delays,
[scenario.delays[0], scenario.delays[1]],
" tickTimersH",
clearSchedule
)
benchTick(
setTickTimeout,
clearTickTimeout,
tick,
delays,
[scenario.delays[0], scenario.delays[1]],
" tickTimersL",
clearSchedule
)
/* // SulfroxNew
benchTick(
sulfNewSetTimeout,
null,
sulfNewTick,
delays,
[scenario.delays[0], scenario.delays[1]],
"SulfroxNew",
clearSchedule
) */
// tickTimers
/* benchTick(
setTickTimeout,
clearTickTimeout,
tickOld,
delays,
[scenario.delays[0], scenario.delays[1]],
TICK_ITERATIONS,
"tickTimers",
clearSchedule
) */
log("")
}
log("done")
}
start()
|