Comments (22)
The challenge is that it's harder to associate the timing data with a specific interaction - AFAIU you'd have to try to correlate timestamps which would get messy.
(Still mostly OT) We use the JS-Self profiling API (+source maps) at Microsoft to correlate the interactions with the event timing api and have plans to open source the code that does this, though open sourcing stuff at Microsoft can take years at times.
from fake-timers.
Having a hard time envisioning when this would be useful ? Seems very exotic.
from fake-timers.
Yeah happy to give a concrete example. This is simplified for discussion purposes, but hopefully you can see how this will apply in a lot of different ways to more complex applications.
Let's say I have a game that progresses days in-game every X milliseconds. So my code looks something like this:
const msPerInGameDay = 100;
let lastDayTime = Date.now();
let gameDay = 0;
setInterval(() => {
if (Date.now() - lastDayTime >= msPerInGameDay) {
gameDay++;
lastDayTime += lastDayTime;
}
}, 25);
This code works by checking if the elapsed time is greater or equal to the amount of time per day, and then incrementing day and moving the marker forward. This is necessary since JavaScript runtimes don't guarantee timeouts to be exact. A 25ms timeout could be fired at 50ms or more. This is especially prevalent on slower systems with fast timeouts or when there's a long-running task (eg: computation on the main thread) that cannot be interrupted when it is time for the interval to fire.
Anyway, this code has a problem. If the browser "backgrounds" the page (depending on the device this might be throttling, or even pausing it entirely) and then returns to the page, the timer will not have executed at the expected rate due to being backgrounded. So given this code, days will progress super fast to catch up. That is jarring to the user to suddenly see the time rapidly ticking up far faster than it normally does.
To handle this, the code in reality is actually the following:
const msPerInGameDay = 100;
let lastDayTime = Date.now();
let gameDay = 0;
setInterval(() => {
if (Date.now() - lastDayTime >= msPerInGameDay) {
gameDay++;
lastDayTime += lastDayTime;
if (Date.now() - lastDayTime >= msPerInGameDay * 5) { // 5 indicating the max number of days that will be caught up
// Handle this in different ways depending on the situation. Might be instantly fast-forwarding time or just skipping to present like this.
lastDayTime = Date.now();
}
}
}, 25);
Okay so now this code handles being backgrounded and will avoid the issue of timers being throttled. In this case, it'll skip all of the time that the user was backgrounded as if it didn't happen.
This all works in a browser and does what is expected. But I now need to write automated tests for this very important functionality. How can I ensure my code does what it is supposed to do in a test? When faking the timers, there is no way to simulate this "jumping forward in time" or "throttling timers" so there is no way to ever hit that inner condition. That is what my feature request is for. A way to simulate this behavior to be able to hit that condition and test this functionality.
Hopefully that all makes sense and seems far less exotic! :)
from fake-timers.
In trying to unblock myself, I found a really hacky way to get the behavior I'm looking for. It is possible to do in userland but is relying on undocumented things. I'm sharing this as an example of what can be implemented in the core library in a really easy, isolated way.
// This uses jest, but same idea applies
function jumpTime(ms: number): void {
for (const timer of Object.values(Date.clock.timers)) {
if (Date.now() + ms > timer.callAt) {
timer.callAt = Date.now() + ms;
}
}
jest.advanceTimersByTime(ms); // Or clock.tick(ms)
}
What this does is go through all of the mocked timers, check if any would have been called during the duration that is being jumped, and if so, set the time at which they'll be next called at to be right at the very end. This way when the clock is ticked, they'll only be processed once and then time will resume after the skip occurred.
I'm happy to make a PR if needed!
from fake-timers.
The idea is fine, but it's essentially a Chromium feature, not a standard. Given its prevalence, though, it seems relevant to include.
Doing this as a non-breaking change is the easiest fit. Not sure if it should be clock.jump or clock.tick(time, { throttle } )
from fake-timers.
I've been doing a lot of research on this behavior. While I'm unable to find an official source for this, from my personal experience (along with many others online) Safari, and especially iOS Safari, is very prone to throttling timers for backgrounded tabs. I don't believe it is Chromium specific. As far as I'm aware, every browser does something similar in some cases, especially when the system is under severe memory pressure (eg: a few hundred/thousand tabs open at once and it'll throttle some tabs).
To look at it from another angle, if you have a setInterval
where the work you are doing inside the interval takes, let's say 1000ms, then the interval won't be able to run faster than once every 1000ms because there's only one thread. So given a slow enough computer and a large enough amount of main-thread work, every browser will experience this even when the tab is in focus.
Anyway haha. I'm too deep into this stuff.
As far as naming, I think that clock.jump
makes the most sense given the semantics of clock.tick
. And I think we should avoid referring to it as throttling
since there's a lot of ways this might occur. However it happens, in the end time has "skipped". So using the jump
/skip
terminology seems more appropriate and easier to understand.
from fake-timers.
The idea is fine, but it's essentially a Chromium feature, not a standard. Given its prevalence, though, it seems relevant to include.
Doing this as a non-breaking change is the easiest fit. Not sure if it should be clock.jump or something like clock.tick(time, { throttle } )
from fake-timers.
@fatso83, where are we at here? Any open questions or concerns before I jump into a PR?
from fake-timers.
Stumbled across this because we have a different use case that would benefit from this:
For our performance metrics, we often want to measure the time when all blocking work triggered by an action has been completed, i.e., painting/rendering + microtasks + any immediate tasks. To do that we do something like:
onButtonClick() {
timeline.start = performance.now();
// Do all the work
// ...
setTimeout(() => {
timeline.end = performance.now();
timeline.log();
});
}
In our testing with Jest, we want to check that this timeline.end
is actually recording the right time. However, using timeline.advanceTimersByTime(100)
will result in timeline.end == timeline.start
because the timeout implicitly has a delay of 0
. But in this case, I'd actually like to test that if the timeout was blocked for a while (while lots of work was happening for rendering), then timeline.end
is called with the appropriate time. I think this concept of jumping forward in time would probably be right (though in our case, it's less jumping, more like simulating the main thread being blocked).
from fake-timers.
Tangent: you probably want to use event timing API to find long frames and long renders (though that would be harder to test since it'd be actual long frames)
from fake-timers.
Yes - we also use that in aggregate. The challenge is that it's harder to associate the timing data with a specific interaction - AFAIU you'd have to try to correlate timestamps which would get messy.
from fake-timers.
@fatso83, where are we at here? Any open questions or concerns before I jump into a PR?
@CreativeTechGuy Sorry, this comment escaped me for months and I cannot remember seeing it before now ... I have absolutely no questions, but I am absolutely not opposed to having something like clock.jump
. I am not sure how I got to be the one to decide (a core problem with open source maintenance), anyway, so go ahead 😄
from fake-timers.
@CreativeTechGuy do you also plan to add this support to Jest?
from fake-timers.
@amorris-canva That is ultimately the goal. I'm not sure the process. I've seen Jest usually adopt those features whenever they update to the latest version of fake-timers. If you have time to do that PR that'd be awesome!
from fake-timers.
@CreativeTechGuy That's not necessarily the case. For instance, you will see countless hits on StackOverflow on how to mix stubbing of timers with promises, which was addressed several years ago in @sinonjs/fake-timers (or lolex back then), with tickAsync()
and friends, but that was only recently exposed on the API in Jest 29.5 (two months ago).
@amorris-canva To state the obvious from the previous paragraph, if you want this in Jest, the quickest path is usually to just make a PR yourself and submit it. When doing this last year (jestjs/jest#12407) it took a couple of months of ping-pong, but eventually you'll get there 😄 That effort needed a bit more hand-holding from Simen, though, so seeing that this is probably just exposing a single method, fetching the latest version and updating the docs, I think you should be good in no time.
from fake-timers.
hi all. I have picked up and started a patch to merge this into jest. The thing blocking taking it across the line is that that the version of fake-timers in npm does not currently include this new method exposed in sinon 15. Are we able to push a new version to npm?
from fake-timers.
@jared-canva Version 11 was pushed in June, some weeks after this landed. Are you sure they haven't been exposed? Maybe just the docs need updating. That usually is the one people fail to address.
from fake-timers.
@jared-canva Just checked. The docs are up to date in both fake-timers and Sinon:
- https://github.com/sinonjs/fake-timers#clocknext--await-clocknextasync
- https://github.com/sinonjs/sinon/blob/main/docs/release-source/release/fake-timers.md#clockjumptime
The versions on NPM also lists 11.0.0, even though 10.3.0 is listed as the last one. I'll fix the metadata to amend that, as it was released after 11, if my memory serves me right.
from fake-timers.
Heh. It is on npm now, when I did the patch the latest version on https://www.npmjs.com/package/@sinonjs/fake-timers was still 10 something, maybe was being hit by an aggressive cache. I had updated my work to just point to github. Perfect thanks.
from fake-timers.
No, it was still there. It was just that the dist-tag
of NPM pointed to 10.3.0 as latest, as I released that after 11.0.0 (as 10.2.0 turned out to be breaking). You can see the publish dates on the version page.
Good that it was sorted out.
from fake-timers.
Ah! that would be why npm.com displayed 10.x as latest then got it.
from fake-timers.
@jared-canva Were you ever able to get it added to Jest? I see that Jest has a pending major version v30 which already updates fake-timers to v11. Seems like it'd be the perfect time to get this .jump()
method exposed! Thanks. 😃
from fake-timers.
Related Issues (20)
- Test failing since Node 18.8.0 HOT 2
- The `shouldAdvanceTime` option seems to cause Jest Test environment to clear during the test HOT 4
- `runToLastAsync` doing infinite loop when `now` is specified to setup clock
- requestAnimationFrame is passed incorrect argument HOT 8
- [Feature]: Speed up all fake timers HOT 12
- [Feature]: In Node, hijack perf_hooks.performance in addition to global.performance HOT 5
- TypeError Since Node 19 HOT 4
- NodeJS "timers" module is not mocked HOT 17
- Calls to `tick()` do not cause all setTimeouts to fire in specific scenarios. HOT 6
- Mock "timers/promise" module HOT 2
- NodeJS `timers` module mock causing tests to timeout in JSDom HOT 3
- process.hrtime gives incorrect results HOT 4
- Leave global scope untouched HOT 5
- How should `shouldAdvanceTime` work together with `Date`? HOT 1
- runAllAsync and runToLastAsync do not run code in the microtask queue HOT 5
- Cleanly switch to real time and back again to fake time while testing HOT 2
- Skypack bundle cannot be loaded in the browser HOT 5
- `install({ toFake: ["setImmediate"] })` fails when `setImmediate` is not available in context (such as browser environment) HOT 5
- Support performance.measure method
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from fake-timers.