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

Fix race condition in animation resolution #1108

Merged
merged 7 commits into from
Nov 10, 2016
Merged

Conversation

rreusser
Copy link
Contributor

@rreusser rreusser commented Nov 2, 2016

This PR solves a minor issue that resulted in race conditions related to animation and relayout. Specifically, if the transition duration and frame duration were identical, it was hit or miss whether the animation would resolve before the transition had completed. The result is that things would happen slightly out of sequence for layout animations in which the relayout that fixes layer transforms would happen after the animation had already resolved. This wasn't a horrible issue, but it made it difficult to expect a specific sequence of events.

One problem this causes is that it will force animations to resolve only after the transition has completed. This is slightly different from the current behavior, but only for the odd (and perhaps invalid) case where the transitions are longer than the frames. I think maybe the transition duration should be clipped to Math.min(transition.duration, frame.duration).

I'd like to sit on this for just a day or two while I work through related issues and make sure there aren't other things going on.

@rreusser rreusser added bug something broken status: in progress labels Nov 2, 2016
@etpinard
Copy link
Contributor

etpinard commented Nov 2, 2016

I think maybe the transition duration should be clipped to Math.min(transition.duration, frame.duration).

👍 that sounds about right to me.

).then(function() {
// Something is delayed a single tick, it seems, so the redraw
// isn't triggered until next tick:
expect(relayouts).toBe(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice test 🎉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. I removed the requestAnimationFrame around this, so the comment is now incorrect. 😭

Copy link
Contributor Author

@rreusser rreusser Nov 8, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@etpinard removed this comment and added a couple other very minor animation tweaks that drop callbacks if the plot is purged before they're executed—which turns out to be a great way to break the tests one out of every hundred times 😬

(The extra fix was mainly added here to get it to run circle-ci again without the failing enterprise thing)

@rreusser rreusser changed the title Add slight modification to animation resolution Fix race condition in animation resolution Nov 9, 2016
// Check only one animating was fired:
expect(cnt).toEqual(1);

// Check unused frames did not affect the current frame:
expect(gd._fullLayout._currentFrame).toEqual('frame0');
}).catch(fail).then(done);
});

it('null frames should not break everything', function(done) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from a manually merged PR, I believe.

@@ -1714,6 +1722,9 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts)
var aborted = false;

function executeTransitions() {

gd.emit('plotly_transitioning', []);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtle, but I didn't like that this was executed asynchronously with the transition actually starting, hence the move.

@@ -2264,7 +2278,7 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
// loop (which may exist as a result of a *different* .animate call)
// still resolves or rejecdts this .animate call's promise. once it's
// complete.
nextFrame.onComplete = resolve;
nextFrame.onComplete = callbackOnNthTime(resolve, 2);
Copy link
Contributor Author

@rreusser rreusser Nov 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this must now be executed in two places before the transition is registered as having completed. That prevents it from resolving before the transition is actually complete. The transition duration is clipped to no greater than the frame duration, but this PR exists because that still didn't ensure that the promise resolved after both had completed, which made ordering difficult to rely upon.

@rreusser
Copy link
Contributor Author

rreusser commented Nov 9, 2016

@etpinard Looking back over it, I'm content with this. I added some explanatory comments and checked it over now that other animation PRs have been merged. The precise ordering and asynchronous details have been a challenge to lock down, so this makes it a bit more robust with frame duration === transition duration. No rush though. It's nice but not urgent.

@etpinard etpinard added this to the v1.20.0 milestone Nov 10, 2016
{layout: {'xaxis.range': [0, 1]}},
{frame: {redraw: true, duration: dur}, transition: {duration: dur}}
).then(function() {
expect(redraws).toBe(1);
Copy link
Contributor

@etpinard etpinard Nov 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if I get this right, before this PR, redraws === 2 here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite, but similar. Sometimes it was zero because the promise resolved before both the frame and the transition were registered as complete. Now the promise doesn't resolve until both are done—which shouldn't be more than a couple ms difference, but that's enough to mess up the ordering.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that makes sense. Thanks for the info!

window.cancelAnimationFrame(gd._transitionData._animationRaf);
if(gd._transitionData) {
// Ensure any dangling callbacks are simply dropped if the plot is purged.
// This is more or less only actually important for testing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loving the honesty here 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aiming for useful comments. 😄

coding

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing hammers this use-case in every conceivable way and only fails once in a rare while, as far as I'm aware only when karma interrupts and restarts the tests. When that happens, it goes kinda haywire and you just have to restart karma. In actual code though, it's so rare that I'd be maybe a little surprised if anyone ever encountered it.

@etpinard
Copy link
Contributor

💃

@rreusser rreusser merged commit ab193b2 into master Nov 10, 2016
@rreusser rreusser deleted the fix-layout-animation branch November 10, 2016 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants