fake-timers icon indicating copy to clipboard operation
fake-timers copied to clipboard

[Feature]: In Node, hijack perf_hooks.performance in addition to global.performance

Open kevinyou opened this issue 2 years ago • 11 comments

  • FakeTimers version : 10.0.2
  • Environment : Node v18.12.1
  • Example URL : https://github.com/kevinyou/wtf-performance-now-mocking/tree/main/sinon-fake-timers
  • Other libraries you are using: N/A

What did you expect to happen? After calling useFakeTimers(), perf_hooks.performance.now() should return a fake time, just like how performance.now() does.

What actually happens 1. perf_hooks.performance.now() is not the same as performance.now(). The latter is correctly hijacked and returns a fake time. 2. perf_hooks.performance.now() returns a negative value -- undefined behavior?

How to reproduce https://github.com/kevinyou/wtf-performance-now-mocking/tree/main/sinon-fake-timers

var perf_hooks = require('perf_hooks');
var FakeTimers = require("@sinonjs/fake-timers");

console.log(perf_hooks.performance === performance); // true
console.log(perf_hooks.performance.now()); // 36.97510700020939
console.log(performance.now()); // 37.08340699970722

var clock = FakeTimers.install({
    now: Date.now(),
    shouldClearNativeTimers: true,
});
console.log(perf_hooks.performance === performance); // now false
console.log(perf_hooks.performance.now()); // -4217337.086744
console.log(performance.now()); // 0

kevinyou avatar Jan 17 '23 03:01 kevinyou

This is starting to get a bit exotic ... Think you could get away with simply faking the required exports yourself?

fatso83 avatar Jan 17 '23 09:01 fatso83

Yes, as a workaround I did that (in Jest) and it works:

jest.mock('perf_hooks', () => ({
    performance: {
        now: () => global.performance.now(),
    },
}));

But as a user, it was very confusing until I dug deep to the fake-timers source code. The documentation talks about overriding performance.now() but whether this applies to the one in perf_hooks was unclear. Also, it wasn't until Node v16 that performance became a global so older libraries still access it through perf_hooks.

Maybe the documentation could have a disclaimer about performance in perf_hooks, like in the config.toFake row notes.

kevinyou avatar Jan 19 '23 00:01 kevinyou

I think it's probably fine to support it and not a lot of work?

I'm happy to make changes in Node if that'd help

benjamingr avatar Jan 24 '23 17:01 benjamingr

@benjamingr I am at a bit of a loss as to how to do this cleanly without hooking into module loader territory, so if you have a way of doing this cleanly by exposing something in Node, then by all means feel free ❤️

fatso83 avatar Jan 24 '23 18:01 fatso83

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Dec 27 '23 10:12 stale[bot]

@benjamingr Know if something for us to hook into will arrive?

fatso83 avatar Aug 25 '24 17:08 fatso83

+1, this will definitely help users of limiter. Particularly since Jest can no longer easily use @kevinyou's workaround with ESM.

dmurvihill avatar Oct 25 '24 00:10 dmurvihill

@dmurvihill If you know how we can do this in some reasonable manner, then feel free to supply a PR. I do not know how. To the best of my knowledge, this would venture into module loader territory, like what I am doing in this article.

On the other hand, node:perf_hooks is a Node API, so maybe Node exposes some hook for us to use, but I would not know.

fatso83 avatar Oct 25 '24 08:10 fatso83

I ended up moving away from the affected dependency entirely, but if it comes up again I'll come back for another look.

dmurvihill avatar Oct 25 '24 20:10 dmurvihill

Never got a reply from Node folks on a related question, so this is requiring a deeper look from someone interested.

fatso83 avatar Oct 28 '24 11:10 fatso83

Kevin's general approach works fine - ESM or no ESM (if you exclude Jest specifics): while the exports are not assignable, their fields are. So we can easily use the same approach in fake-timers:

 const perf_hooks = await import("perf_hooks")
perf_hooks.performance.now = () => 42

Should not be much work. I just (erroneously) assumed we needed to replace the entire module.

fatso83 avatar Dec 11 '24 22:12 fatso83