-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(animationFrames): emit the timestamp from the rAF's callback #5438
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for opening the PR. There are a couple of things that I think need to be addressed.
@@ -77,20 +77,23 @@ export interface TimestampProvider { | |||
* | |||
* @param timestampProvider An object with a `now` method that provides a numeric timestamp | |||
*/ | |||
export function animationFrames(timestampProvider: TimestampProvider = Date) { | |||
export function animationFrames(timestampProvider?: TimestampProvider) { | |||
return timestampProvider === Date ? DEFAULT_ANIMATION_FRAMES : animationFramesFactory(timestampProvider); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default - DEFAULT_ANIMATION_FRAMES
- should be changed to not use Date
as the timestamp provider, IMO. The purpose of the default is to avoid creating observable instances when animationFrames
is called with the expected/default timestamp provider and the default is to use the timestamp provided by the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I did forget to change the default here.
if (start === null) { | ||
start = timestampProvider ? timestampProvider.now() : timestamp; | ||
} | ||
subscriber.next((timestampProvider ? timestampProvider.now() : timestamp) - start); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the timestamps provided by the API are being used, IMO they should be passed through without modification.
That is, the start
should only be subtracted when using a caller-specified timestamp provider.
AFAICT, the point of using the API-provider timestamp is to synchronize animations and if an elapsed time is calculated from the start
time, synchronization would only be possible between animations that started on the same animation frame. This seems like an unnecessary restriction, to me.
@benlesh What is your opinion on this? When should the operator emit an elapsed time: always, never or only if a caller-specified timestamp provider is specified?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. So you'd have users calculate the elapsed time themselves?
The problem I see is they would then need to capture the initial time themselves. Maybe we can provide both? { timestamp: number, elapsed: number }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then again, if they already have the timestamp provider, it's trivial to get the current timestamp within whatever operation you're performing. But less trivial to know the recipe to get the elapsed time, which is more useful in animations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think providing both - { timestamp: number, elapsed: number }
- would be the best. It'll cover all bases and is essentially self-documenting. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks.
I'm not convinced that this is a solid change. With this change, we are back to world where people need to know a recipe in order to get the time that has elapsed since the beginning of the observable. Which is what you usually need when you're doing animations. That, in the face of the fact that it's as simple as calling I suppose as a compromise we could add an operator provided the elapsed time. But that's just one more thing, right? I'm not sure I like that either. I'm not saying I'm against this change. I'm just saying that I'm not convinced. |
I'm not sure what's meant be 'recipe' here. If the value emitted from the rAF observable has both the |
Ah. You know what, I was reviewing this from my phone. I missed that. My mistake. I'll think about it some more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some minor changes.
I'm VERY sorry about any confusion earlier with my hasty reviews. I misunderstood the intent.
const run = (timestamp: DOMHighResTimeStamp | number) => { | ||
const currentTimestamp = timestampProvider ? timestampProvider.now() : timestamp; | ||
if (start === null) { | ||
start = currentTimestamp; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, the only issue I have with this now is that the start
time is now non-deterministic. I think collection of start
should be moved back to where it was. This is no longer the "start" it's the first animation frame.
Consider the following scenario: A user triggers an animation that is supposed to take 30 seconds, then, before the animation frame fires they switch tabs. If the start isn't collected until the first animationFrame, when the user tabs back, they'll be forced to watch the full animation.
If the start
is collected when the observable is subscribed to, when they come back, the animation will be done as expected.
I understand that maybe this change probably arose from the confusion about it being a DOMHighResTimeStamp, and the code was previously using Date
as default timestamp provider. Instead, it's safe to use performance
as the default TimestampProvider, as it is supported by all browsers and platforms we currently support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to perf time as the default for animation frames, per https://www.w3.org/TR/hr-time-2/#introduction subtracting 2 ntp times is not safe, it can result in negative durations
@cartant We might want to consider updating |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I'm sorry again, but given the implications on testing this using a TestScheduler
's run
method, I'm not sure we can use rAF's values from it's callback. We may just have to use a light wrapper around performance.now()
which should be the same thing.
This is because the TestScheduler needs to be able to control what is returned by now()
for deterministic testing. Although, I'll admit that doing this for animation frames is a bit of an uncharted territory for the test scheduler, it is currently doing this for the animationFrameScheduler
, for better or worse.
(Really, really sorry, haha... I promise I'm not intentionally trying to be a PITA on this one) |
No worries. I totally understand why these things are problematic. I will try to update the PR soon. |
@tmair I don't think there's anything that really needs to be updated in this PR, ATM. The |
Keep in mind:
Sampling the time with performance.now beats using Date.now, but can still cause animations to be out of sync from each other in a give animation frame, because of this part specifically:
which can affect the animation & it make it look not smooth. Unless the time is sampled once per animation frame, which RAF's callback provided DOMHighResTime already handles. If we're sampling a timestampProvider, we should ideally add the same affordance the native RAF callback has. |
ca6c077
to
d7c7c0b
Compare
// queued for a considerable period of time and the elapsed time should | ||
// represent the time elapsed since subscription - not the time since the | ||
// first rendered animation frame. | ||
const start = provider.now(); | ||
const run = (timestamp: DOMHighResTimeStamp | number) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this still doesn't really solve the problem that this is untestable in TestScheduler run mode, which automatically patches our schedulers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the solution here is going to be creating a lightweight wrapper around rAF, such that we can override it in run
mode. That's what the animationFrameScheduler is supposed to be, but it's inadequate, because it doesn't provide the timestamp, and our schedulers in general are perhaps a bit overkill for this use case.
Something like this should suffice:
export const rAF = {
schedule(callback: (timestamp: number) => void): Subscription {
const id = this.impl(callback);
return new Subscription(() => cancelAnimationFrame(id));
},
impl: requestAnimationFrame,
}
Then, in our run
mode, we just need to pull that in and patch the impl
to override it's behavior where we are using it.
We could also just monkey patch rAF directly, but that has other implications I'm not keen on.
We also need some sort of way to declare when animation frames should be fired in a test. Probably another, idempotent helper that we add to the context passed to TestScheduler.run
. like:
rxTest.run(({ cold, expectObservable, animationFrames }) => {
animationFrames('-----x----x----x----x---x-----');
// rest of the test here
});
// queued for a considerable period of time and the elapsed time should | ||
// represent the time elapsed since subscription - not the time since the | ||
// first rendered animation frame. | ||
const start = provider.now(); | ||
const run = (timestamp: DOMHighResTimeStamp | number) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the solution here is going to be creating a lightweight wrapper around rAF, such that we can override it in run
mode. That's what the animationFrameScheduler is supposed to be, but it's inadequate, because it doesn't provide the timestamp, and our schedulers in general are perhaps a bit overkill for this use case.
Something like this should suffice:
export const rAF = {
schedule(callback: (timestamp: number) => void): Subscription {
const id = this.impl(callback);
return new Subscription(() => cancelAnimationFrame(id));
},
impl: requestAnimationFrame,
}
Then, in our run
mode, we just need to pull that in and patch the impl
to override it's behavior where we are using it.
We could also just monkey patch rAF directly, but that has other implications I'm not keen on.
We also need some sort of way to declare when animation frames should be fired in a test. Probably another, idempotent helper that we add to the context passed to TestScheduler.run
. like:
rxTest.run(({ cold, expectObservable, animationFrames }) => {
animationFrames('-----x----x----x----x---x-----');
// rest of the test here
});
@benlesh To separate it from the annoying weirdness of the |
Okay, LGTM, in that I think it's a good iteration, but we have failing tests now that need cleared up. |
@cartant Just because I wasn't being clear. I'm good with these changes, the only thing stopping the merge is getting the tests working. |
@benlesh |
I guess just mark the tests as skipped for now |
Donesies |
Emit the timestamp provided by rAF to align better with the rAF specification. Multiple scheduled rAF callbacks within the same frame will be called with the same timestamp.
And use performance.now() for the elapsed calculation instead of the passed timestamp - see comment for explanation.
a6a10d0
to
54b4327
Compare
Description:
Emit the timestamp provided by rAF to align better with the rAF
specification. Multiple scheduled rAF callbacks within the same frame
will be called with the same timestamp.
Related issue (if exists):
This fixes #5194
Further notes
I kept the possibility to supply a custom
TimestampProvider
to theanimationFrames
function.