From b494bf1d89ad028a63d605fcdbe6e84f055ce233 Mon Sep 17 00:00:00 2001 From: flakey5 <73616808+flakey5@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:52:29 -0800 Subject: [PATCH] lib: add `nowAbsolute` to fast timers Signed-off-by: flakey5 <73616808+flakey5@users.noreply.github.com> --- benchmarks/timers/now-absolute.mjs | 13 +++++++++++ lib/cache/memory-cache-store.js | 3 ++- lib/handler/cache-handler.js | 5 +++-- lib/interceptor/cache.js | 5 +++-- lib/util/timers.js | 20 ++++++++++++++++- test/cache-interceptor/cache-stores.js | 31 +++++++++++++------------- test/interceptors/cache.js | 3 +++ test/timers.js | 12 ++++++++++ 8 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 benchmarks/timers/now-absolute.mjs diff --git a/benchmarks/timers/now-absolute.mjs b/benchmarks/timers/now-absolute.mjs new file mode 100644 index 00000000000..ef58363db50 --- /dev/null +++ b/benchmarks/timers/now-absolute.mjs @@ -0,0 +1,13 @@ +import { bench, group, run } from 'mitata' +import timers from '../../lib/util/timers.js' + +group(() => { + bench('Date.now()', () => { + Date.now() + }) + bench('nowAbsolute()', () => { + timers.nowAbsolute() + }) +}) + +await run() diff --git a/lib/cache/memory-cache-store.js b/lib/cache/memory-cache-store.js index 4181cbbf9ca..4c96af8860f 100644 --- a/lib/cache/memory-cache-store.js +++ b/lib/cache/memory-cache-store.js @@ -1,6 +1,7 @@ 'use strict' const { Writable } = require('node:stream') +const { nowAbsolute } = require('../util/timers.js') /** * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey @@ -76,7 +77,7 @@ class MemoryCacheStore { const topLevelKey = `${key.origin}:${key.path}` - const now = Date.now() + const now = nowAbsolute() const entry = this.#entries.get(topLevelKey)?.find((entry) => ( entry.deleteAt > now && entry.method === key.method && diff --git a/lib/handler/cache-handler.js b/lib/handler/cache-handler.js index bea9e775a50..e39c5479ac7 100644 --- a/lib/handler/cache-handler.js +++ b/lib/handler/cache-handler.js @@ -6,6 +6,7 @@ const { parseCacheControlHeader, parseVaryHeader } = require('../util/cache') +const { nowAbsolute } = require('../util/timers.js') function noop () {} @@ -121,7 +122,7 @@ class CacheHandler extends DecoratorHandler { return downstreamOnHeaders() } - const now = Date.now() + const now = nowAbsolute() const staleAt = determineStaleAt(now, headers, cacheControlDirectives) if (staleAt) { const varyDirectives = this.#cacheKey.headers && headers.vary @@ -300,7 +301,7 @@ function determineStaleAt (now, headers, cacheControlDirectives) { // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3 const expiresDate = new Date(headers.expire) if (expiresDate instanceof Date && !isNaN(expiresDate)) { - return now + (Date.now() - expiresDate.getTime()) + return now + (nowAbsolute() - expiresDate.getTime()) } } diff --git a/lib/interceptor/cache.js b/lib/interceptor/cache.js index cb0bc6fa650..9895fcb8a86 100644 --- a/lib/interceptor/cache.js +++ b/lib/interceptor/cache.js @@ -7,6 +7,7 @@ const CacheHandler = require('../handler/cache-handler') const MemoryCacheStore = require('../cache/memory-cache-store') const CacheRevalidationHandler = require('../handler/cache-revalidation-handler') const { assertCacheStore, assertCacheMethods, makeCacheKey } = require('../util/cache.js') +const { nowAbsolute } = require('../util/timers.js') const AGE_HEADER = Buffer.from('age') @@ -101,7 +102,7 @@ module.exports = (opts = {}) => { if (typeof handler.onHeaders === 'function') { // Add the age header // https://www.rfc-editor.org/rfc/rfc9111.html#name-age - const age = Math.round((Date.now() - cachedAt) / 1000) + const age = Math.round((nowAbsolute() - cachedAt) / 1000) // TODO (fix): What if rawHeaders already contains age header? rawHeaders = [...rawHeaders, AGE_HEADER, Buffer.from(`${age}`)] @@ -133,7 +134,7 @@ module.exports = (opts = {}) => { } // Check if the response is stale - const now = Date.now() + const now = nowAbsolute() if (now < result.staleAt) { // Dump request body. if (util.isStream(opts.body)) { diff --git a/lib/util/timers.js b/lib/util/timers.js index c15bbc37aa1..c948d57b067 100644 --- a/lib/util/timers.js +++ b/lib/util/timers.js @@ -21,6 +21,13 @@ */ let fastNow = 0 +/** + * The fastNowAbsolute variable contains the rough result of Date.now() + * + * @type {number} + */ +let fastNowAbsolute = Date.now() + /** * RESOLUTION_MS represents the target resolution time in milliseconds. * @@ -122,6 +129,8 @@ function onTick () { */ fastNow += TICK_MS + fastNowAbsolute = Date.now() + /** * The `idx` variable is used to iterate over the `fastTimers` array. * Expired timers are removed by replacing them with the last element in the array. @@ -390,6 +399,9 @@ module.exports = { now () { return fastNow }, + nowAbsolute () { + return fastNowAbsolute + }, /** * Trigger the onTick function to process the fastTimers array. * Exported for testing purposes only. @@ -419,5 +431,11 @@ module.exports = { * Marking as deprecated to discourage any use outside of testing. * @deprecated */ - kFastTimer + kFastTimer, + /** + * Exporting for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + RESOLUTION_MS } diff --git a/test/cache-interceptor/cache-stores.js b/test/cache-interceptor/cache-stores.js index dfa521fa0d1..ef48e11c713 100644 --- a/test/cache-interceptor/cache-stores.js +++ b/test/cache-interceptor/cache-stores.js @@ -5,6 +5,7 @@ const { deepStrictEqual, notEqual, equal } = require('node:assert') const { Readable } = require('node:stream') const { once } = require('node:events') const MemoryCacheStore = require('../../lib/cache/memory-cache-store') +const { nowAbsolute } = require('../../lib/util/timers.js') cacheStoreTests(MemoryCacheStore) @@ -32,9 +33,9 @@ function cacheStoreTests (CacheStore) { statusCode: 200, statusMessage: '', rawHeaders: [Buffer.from('1'), Buffer.from('2'), Buffer.from('3')], - cachedAt: Date.now(), - staleAt: Date.now() + 10000, - deleteAt: Date.now() + 20000 + cachedAt: nowAbsolute(), + staleAt: nowAbsolute() + 10000, + deleteAt: nowAbsolute() + 20000 } const requestBody = ['asd', '123'] @@ -71,9 +72,9 @@ function cacheStoreTests (CacheStore) { statusCode: 200, statusMessage: '', rawHeaders: [Buffer.from('1'), Buffer.from('2'), Buffer.from('3')], - cachedAt: Date.now(), - staleAt: Date.now() + 10000, - deleteAt: Date.now() + 20000 + cachedAt: nowAbsolute(), + staleAt: nowAbsolute() + 10000, + deleteAt: nowAbsolute() + 20000 } const anotherBody = ['asd2', '1234'] @@ -108,9 +109,9 @@ function cacheStoreTests (CacheStore) { statusCode: 200, statusMessage: '', rawHeaders: [Buffer.from('1'), Buffer.from('2'), Buffer.from('3')], - cachedAt: Date.now() - 10000, - staleAt: Date.now() - 1, - deleteAt: Date.now() + 20000 + cachedAt: nowAbsolute() - 10000, + staleAt: nowAbsolute() - 1, + deleteAt: nowAbsolute() + 20000 } const requestBody = ['part1', 'part2'] @@ -140,9 +141,9 @@ function cacheStoreTests (CacheStore) { const requestValue = { statusCode: 200, statusMessage: '', - cachedAt: Date.now() - 20000, - staleAt: Date.now() - 10000, - deleteAt: Date.now() - 5 + cachedAt: nowAbsolute() - 20000, + staleAt: nowAbsolute() - 10000, + deleteAt: nowAbsolute() - 5 } const requestBody = ['part1', 'part2'] @@ -174,9 +175,9 @@ function cacheStoreTests (CacheStore) { vary: { 'some-header': 'hello world' }, - cachedAt: Date.now(), - staleAt: Date.now() + 10000, - deleteAt: Date.now() + 20000 + cachedAt: nowAbsolute(), + staleAt: nowAbsolute() + 10000, + deleteAt: nowAbsolute() + 20000 } const requestBody = ['part1', 'part2'] diff --git a/test/interceptors/cache.js b/test/interceptors/cache.js index 5e7b3baedb9..954a009c870 100644 --- a/test/interceptors/cache.js +++ b/test/interceptors/cache.js @@ -6,6 +6,7 @@ const { createServer } = require('node:http') const { once } = require('node:events') const FakeTimers = require('@sinonjs/fake-timers') const { Client, interceptors, cacheStores } = require('../../index') +const { tick } = require('../../lib/util/timers') describe('Cache Interceptor', () => { test('doesn\'t cache request w/ no cache-control header', async () => { @@ -160,6 +161,7 @@ describe('Cache Interceptor', () => { const clock = FakeTimers.install({ shouldClearNativeTimers: true }) + tick(0) const server = createServer((req, res) => { res.setHeader('cache-control', 'public, s-maxage=1, stale-while-revalidate=10') @@ -205,6 +207,7 @@ describe('Cache Interceptor', () => { strictEqual(await response.body.text(), 'asd') clock.tick(1500) + tick(1500) // Now we send two more requests. Both of these should reach the origin, // but now with a conditional header asking if the resource has been diff --git a/test/timers.js b/test/timers.js index 9d8cd596e90..b067e61f5e6 100644 --- a/test/timers.js +++ b/test/timers.js @@ -255,4 +255,16 @@ describe('timers', () => { await t.completed }) + + test('nowAbsolute', (t) => { + t = tspl(t, { plan: 1 }) + + const actualNow = Date.now() + + const lowerBound = actualNow - timers.RESOLUTION_MS + const upperBound = actualNow + timers.RESOLUTION_MS + const fastNowAbsolute = timers.nowAbsolute() + + t.equal(fastNowAbsolute >= lowerBound && fastNowAbsolute <= upperBound, true) + }) })