Skip to content
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

cancelAnimationFrame should also cancel pending callbacks #4359

Closed
egirard opened this issue Feb 12, 2019 · 18 comments · Fixed by #4375
Closed

cancelAnimationFrame should also cancel pending callbacks #4359

egirard opened this issue Feb 12, 2019 · 18 comments · Fixed by #4375
Labels
interop Implementations are not interoperable with each other

Comments

@egirard
Copy link

egirard commented Feb 12, 2019

requestAnimationFrame callbacks cannot reliably cancel other requestAnimationFrame callbacks.

Calls to requestAnimationFrame will not add callbacks to the current callback loop, but instead adds them to the next callback loop. This allows callbacks to re-register themselves if they need to continue operating, which is a very common pattern for developers.

cancelAnimationFrame follows the same pattern, and as a result requestAnimationFrame callbacks have somewhat unpredictable results if they make calls to cancelAnimationFrame.

For example, assume a page has called requestAnimationFrame with rAF-callback1 and rAF-callback2, each of which loops by calling rAF on themselves at the end of the callback.

  • if rAF-callback2 calls cancelAnimationFrame on rAF-callback1, then callback1 will be removed from future callbacks. The cancelAnimationFrame call succeeds.
  • if rAF-callback1 calls cancelAnimationFrame on rAF-callback2, the current (pending) call to callback2 continues, and the cancelAnimationFrame call has no effect.

Recommend that we amend the spec so that calls to cancelAnimationFrame also cancel pending callbacks within the current callback loop.

Many browsers (including Chromium, Edge, Safari) already follow this implementation. Previous discussion on this issue can be found in mozilla and chromium

@annevk annevk added the interop Implementations are not interoperable with each other label Feb 13, 2019
@annevk
Copy link
Member

annevk commented Feb 13, 2019

@jakearchibald
Copy link
Collaborator

jakearchibald commented Feb 13, 2019

To summarise:

function one() {
  console.log('one');
  cancelAnimationFrame(twoHandle);
}

function two() {
  console.log('two');
}

const oneHandle = requestAnimationFrame(one);
const twoHandle = requestAnimationFrame(two);

Should "two" be logged? Demo.

Chrome, Edge, Safari say no. Firefox and the spec say yes.


In the spec cancelAnimationFrame removes items from the map of animation frame callbacks. However, when the event loop processes animation frames, it clones the map of callbacks, so it's out-of-scope for cancelAnimationFrame's removals.

This feels unintuitive for me. For instance:

let animOneHandle;
let animTwoHandle;

function animOne() {
  // …
  animOneHandle = requestAnimationFrame(animOne);
}

function animTwo() {
  // …
  animTwoHandle = requestAnimationFrame(animTwo);
}

function startAnims() {
  animOneHandle = requestAnimationFrame(animOne);
  animTwoHandle = requestAnimationFrame(animTwo);
}

function stopAnims() {
  cancelAnimationFrame(animOneHandle);
  cancelAnimationFrame(animTwoHandle);
}

With this pattern, stopAnims will stop the next call to animOne and animTwo sometimes, depending on when it's called.

I also find Flackr's comparison to event listeners compelling.

In terms of updating the spec, instead of cloning the map, it should clone the keys. Then, it can get each callback from the original map, skipping it if it's missing. Items are removed from the original map after each callback.

@jakearchibald
Copy link
Collaborator

jakearchibald commented Feb 13, 2019

I asked Twitter, and I'm really surprised at how close the vote is https://twitter.com/jaffathecake/status/1095662236230733824.

However, it seems like there's a lot of confusion around how animation callbacks are queued. A lot of folks think the order is random, or they happen in parallel somehow. Oh, and a few folks seem to think it's a scoping issue.

@rocallahan
Copy link

I think the first comment here is a compelling argument for changing the spec.

Originally I didn't have cancelAnimationFrame, instead people who wanted to cancel could set up their own boolean cancellation flag and just bail out of the callback if that flag is set. Would have avoided this problem :-).

@domenic
Copy link
Member

domenic commented Feb 13, 2019

It sounds like we should change this (great find!) but I'd love to hear from Firefox folks before changing the spec out from under them. Let's hope @birtles can chime in soon.

@flackr
Copy link

flackr commented Feb 13, 2019

Another case for the proposed behavior is consistency with setTimeout, which has the same semantics in this demo. I believe both a and b's time has come up when a is called, but b is still canceled by a.

@jakearchibald
Copy link
Collaborator

Each setTimeout callback is another turn of the event loop, so the event listener example seems a lot closer.

@wanderview
Copy link
Member

Each setTimeout callback is another turn of the event loop, so the event listener example seems a lot closer.

FWIW setTimeout callbacks can be executed in batch within a single event loop turn. I don't think the spec explicitly calls this out, but it vaguely allows it by having a separate timer task source.

@birtles
Copy link
Contributor

birtles commented Feb 13, 2019

I agree that the first comment is particularly compelling. I'm ok with this change.

@annevk
Copy link
Member

annevk commented Feb 14, 2019

@wanderview how does that allow for it? What you're suggesting would mean a Promise.resolve().then(...) would not necessarily run at the end of a timeout, which is not something the specification allows for currently. File a new issue if that's indeed the case?

@jakearchibald
Copy link
Collaborator

You always get a microtask checkpoint when the stack empties, so if a browser were running multiple timeout callbacks in a single task it wouldn't change promise behaviour.

But yeah the spec doesn't seem to allow this grouping.

@annevk
Copy link
Member

annevk commented Feb 14, 2019

In that case (executing two in a single task vs each in their own task not being observable) @wanderview is correct as they do share a task source and the user agent is allowed to prioritize that task source over others.

@wanderview
Copy link
Member

FWIW, I tried to removing this batching to improve interactivity and ran into web compat issues. See this post for more details.

(Sorry for the thread hijack regarding this.)

@domenic domenic assigned domenic and unassigned domenic Feb 14, 2019
@domenic
Copy link
Member

domenic commented Feb 14, 2019

Sounds like we have agreement. Is anyone willing to work on a spec change and tests PR? I wasn't able to gather the exact model for the spec change proposed; I thought maybe it was just not doing the clone, but Chrome at least seems to have two callback collections (callbacks_ and callbacks_to_invoke_), which is more complicated.

@birtles
Copy link
Contributor

birtles commented Feb 14, 2019

Yes, we don't want to drop the cloning, otherwise calls to requestAnimationFrame from within a rAF callback would end up running in the same frame. @jakearchibald proposed the spec behavior earlier:

In terms of updating the spec, instead of cloning the map, it should clone the keys. Then, it can get each callback from the original map, skipping it if it's missing. Items are removed from the original map after each callback.

It looks like getting the keys of an ordered map already creates a new ordered set so if we want to avoid having two callback collections like in Chrome, I guess we could do something like:

  1. Let callbacks be target's map of animation frame callbacks.
  2. Let callback handles be the result of getting the keys of callbacks.
  3. For each handle in callback handles, if callbacks contains handle, perform the following steps:
    1. Let callback be callbacks[handle].
    2. Remove callbacks[handle].
    3. Invoke callback, passing now as the only argument, and if an exception is thrown, report the exception.

Does that seem right?

@annevk
Copy link
Member

annevk commented Feb 15, 2019

Looks good to me.

@egirard
Copy link
Author

egirard commented Feb 15, 2019

@birtles suggestion looks good to me.

On a side note, the existing blink implementation uses a cancelled flag (similar to Event.cancelled) and tests that flag before calling each callback. This avoids duplicating the collection.

@birtles
Copy link
Contributor

birtles commented Feb 18, 2019

Thanks for the feedback. I'll work on a PR and WPT for this.

birtles added a commit to birtles/html that referenced this issue Feb 19, 2019
Calling cancelAnimationFrame from within a requestAnimationFrame
callback should cancel any requestAnimationFrame callbacks that are
pending for the current frame. The currently specified behavior does not
permit that, however, since it clones the set of callbacks before
iterating over them.

This patch updates the algorithm to run animation frame callbacks such
that it is possible to cancel a pending animation frame callback.
This also brings the specified behavior into line with its
implementation in Blink, EdgeHTML, and WebKit.

This closes whatwg#4359.
annevk pushed a commit that referenced this issue Feb 19, 2019
Calling cancelAnimationFrame() from within a requestAnimationFrame()
callback should cancel any animation frame callbacks that are
pending for the current frame. The currently specified behavior does not
permit that, however, since it clones the set of callbacks before
iterating over them.

This patch updates the algorithm to run animation frame callbacks such
that it is possible to cancel a pending animation frame callback.
This also brings the specified behavior into line with its
implementation in Blink, EdgeHTML, and WebKit.

Tests: web-platform-tests/wpt#15455.

Closes #4359.
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Feb 28, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

--HG--
extra : rebase_source : ff3c69a82f0ef61f562edd1610017a17c8f26276
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Feb 28, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

--HG--
extra : moz-landing-system : lando
mykmelez pushed a commit to mykmelez/gecko that referenced this issue Feb 28, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974
mykmelez pushed a commit to mykmelez/gecko that referenced this issue Feb 28, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974
moz-v2v-gh pushed a commit to mozilla/gecko-dev that referenced this issue Apr 5, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

--HG--
extra : rebase_source : 696dde4f205f2d12a67a4e9649d629a0f4e7de27
xeonchen pushed a commit to xeonchen/gecko that referenced this issue Apr 5, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: 65c438efa01fc8478aa3d1d69bab22f758038b62
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: da0f977287bdc5fc72623f644953d9da00fbd38c
gecko-dev-updater pushed a commit to marco-c/gecko-dev-comments-removed that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: 65c438efa01fc8478aa3d1d69bab22f758038b62
gecko-dev-updater pushed a commit to marco-c/gecko-dev-comments-removed that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: da0f977287bdc5fc72623f644953d9da00fbd38c
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: 65c438efa01fc8478aa3d1d69bab22f758038b62
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: da0f977287bdc5fc72623f644953d9da00fbd38c
gecko-dev-updater pushed a commit to marco-c/gecko-dev-comments-removed that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: 70dd07f15ae1548c63c80d85470246f98d8d58a3
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: 70dd07f15ae1548c63c80d85470246f98d8d58a3
gecko-dev-updater pushed a commit to marco-c/gecko-dev-wordified-and-comments-removed that referenced this issue Oct 4, 2019
…e callback scheduled in the same frame; r=farre

As per the following change to the HTML spec:

  whatwg/html@86b05f8

when running a requestAnimationFrame callback it should be possible to cancel
another requestAnimationFrame callback scheduled to run in the same frame by
using cancelAnimationFrame.

See issue:

  whatwg/html#4359

Differential Revision: https://phabricator.services.mozilla.com/D20974

UltraBlame original commit: 70dd07f15ae1548c63c80d85470246f98d8d58a3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interop Implementations are not interoperable with each other
Development

Successfully merging a pull request may close this issue.

8 participants