sinonjs / fake-timers

Fake setTimeout and friends (collectively known as "timers"). Useful in your JavaScript tests. Extracted from Sinon.JS
BSD 3-Clause "New" or "Revised" License
794 stars 103 forks source link

ERR_PERFORMANCE_INVALID_TIMESTAMP triggered by tickAsync #430

Closed Eomm closed 2 years ago

Eomm commented 2 years ago

What did you expect to happen?

The tickAsync's call does not trigger an error and modifies the process timer.

What actually happens

An error is thrown:

TypeError [ERR_PERFORMANCE_INVALID_TIMESTAMP]: -940795691.799375 is not a valid timestamp
    at new NodeError (node:internal/errors:371:5)
    at new PerformanceMark (node:internal/perf/usertiming:68:13)
    at EventTarget.mark (node:internal/perf/usertiming:94:16)
    at Immediate.<anonymous> (/Users/mspigolon/workspace/_experiments/fastify-routes-stats/asd.js:17:15)
    at processImmediate (node:internal/timers:464:21) {
  code: 'ERR_PERFORMANCE_INVALID_TIMESTAMP'
}

How to reproduce


const fakeTimer = require('@sinonjs/fake-timers')
const setTimeoutPromise = require('util').promisify(setTimeout)

const { performance, PerformanceObserver } = require('perf_hooks')

let history
const obs = new PerformanceObserver((items) => {
  const fetchedItems = items.getEntries()
  history = fetchedItems
  performance.clearMarks()
})
obs.observe({ entryTypes: ['measure'], buffered: true })

performance.mark('a')
process.nextTick(() => {
  performance.mark('b')
  performance.measure('a to b', 'a', 'b')
})

async function works () {
  await setTimeoutPromise(1)
  console.log({ history })
}

async function doesNotWorks () {
  const clock = fakeTimer.install()
  await clock.tickAsync(35000)
  console.log({ history })
}

// works()
doesNotWorks()
fatso83 commented 2 years ago

Lolex fakes the performance timers by default. If you do not want that, then specify what you what in the toFake parameter. See the docs

Eomm commented 2 years ago

I agree with that, but don't you think that the fake data should not trigger the ERR_PERFORMANCE_INVALID_TIMESTAMP error?

fatso83 commented 2 years ago

AFAIK, you are mixing real and fake versions of the same APIs. Expecting them to work together is unreasonable: it is either/or. In one world you are dealing with a clock set to 1970 and in the other you are dealing with a constantly changing clock and you are then expecting timers living in one world to create timestamps that are valid in the other. This is not something I can see how we could reasonably support :)

In essence: the error is not triggered by any bug in the fake-timers library. It is caused by faking the performance timers and then supplying data from those into references to the original ones captured before the stubbing. That is simply usage error, so a WONT_FIX in my book.

This will never work:

const fakeTimer = require('@sinonjs/fake-timers')
const setTimeoutPromise = require('util').promisify(setTimeout)

const { performance, PerformanceObserver } = require('perf_hooks')

You are here capturing references to the original objects that you later try to "shade"/override using fake-timers. Anything done with fake-timers later on will simply not work as intendend.

You basically need to do this:

  1. Install the fake timers
  2. Capture/use references to the various timers and/or performance objects

If you do it in the other order, nothing @sinonjs/fake-timers do will be able to change your references. For instance:

require('util').promisify(setTimeout)

This binds the non-faked setTimeout to setTimeoutPromise. This means that doing setTimeoutPromise(fn,100) will not be affected by doing clock.tickAsync(100), since that clocks only affects the newly stubbed setTimeout.

Feel free if you have any more questions.