Coder Social home page Coder Social logo

Comments (22)

benjamingr avatar benjamingr commented on August 16, 2024 2

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.

fatso83 avatar fatso83 commented on August 16, 2024

Having a hard time envisioning when this would be useful ? Seems very exotic.

from fake-timers.

CreativeTechGuy avatar CreativeTechGuy commented on August 16, 2024

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.

CreativeTechGuy avatar CreativeTechGuy commented on August 16, 2024

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.

fatso83 avatar fatso83 commented on August 16, 2024

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.

CreativeTechGuy avatar CreativeTechGuy commented on August 16, 2024

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.

fatso83 avatar fatso83 commented on August 16, 2024

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.

CreativeTechGuy avatar CreativeTechGuy commented on August 16, 2024

@fatso83, where are we at here? Any open questions or concerns before I jump into a PR?

from fake-timers.

amorris-canva avatar amorris-canva commented on August 16, 2024

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.

benjamingr avatar benjamingr commented on August 16, 2024

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.

amorris-canva avatar amorris-canva commented on August 16, 2024

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 avatar fatso83 commented on August 16, 2024

@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.

amorris-canva avatar amorris-canva commented on August 16, 2024

@CreativeTechGuy do you also plan to add this support to Jest?

from fake-timers.

CreativeTechGuy avatar CreativeTechGuy commented on August 16, 2024

@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.

fatso83 avatar fatso83 commented on August 16, 2024

@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.

jared-canva avatar jared-canva commented on August 16, 2024

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.

fatso83 avatar fatso83 commented on August 16, 2024

@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.

fatso83 avatar fatso83 commented on August 16, 2024

@jared-canva Just checked. The docs are up to date in both fake-timers and Sinon:

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.

jared-canva avatar jared-canva commented on August 16, 2024

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.

fatso83 avatar fatso83 commented on August 16, 2024

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.
image

Good that it was sorted out.

from fake-timers.

jared-canva avatar jared-canva commented on August 16, 2024

Ah! that would be why npm.com displayed 10.x as latest then got it.

from fake-timers.

CreativeTechGuy avatar CreativeTechGuy commented on August 16, 2024

@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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.