Jump to content
GreenSock

avancamp

.call being skipped when timeScale is very high

Recommended Posts

I am potentially close to finding a repro.

 

First question: is it intentional that gsap.globalTimeline.progress() can sometimes grow unbounded as demonstrated in this CodePen? (check the console output)

 

EDIT: On first run, the issue doesn't seem to happen? Try hitting Run again and it seems to happen then.

 

EDIT 2: The issue is only present if the JS Console is open when Run is pressed... I do not understand.

 

EDIT 3: This issue is... hardware dependent somehow? It doesn't happen on my laptop, but does on my desktop. I am so confused.

 

EDIT 4: It has happened on my laptop exactly once after a few dozen tries. It happens on my desktop every time consistently.

 

See the Pen WNvvRap?editors=0010 by Lange (@Lange) on CodePen

Link to comment
Share on other sites

Yeah, the playheads of all timelines/tweens are limited to movement within the bounds of 0 and their duration EXCEPT for the globalTimeline. By necessity, that must be able to progress endlessly. That's why progress() is returning odd values for you but I can add some clamping to that just so that progress()/totalProgress() can't ever be reported as beyond 1 even on the globalTimeline. However, that doesn't mean the playhead won't actually go beyond where the last child tween sits. 

 

Does that clear things up? 

  • Like 1
Link to comment
Share on other sites

Adding a clamp might help, but I worry that there's something deeper that I'm not understanding. Particularly: why does this CodePen behave differently under different browser conditions? Sometimes progress grows unbounded, sometimes it doesn't.

Link to comment
Share on other sites

Potentially related, this code will return NaN, which I think ends up propagating through several parts of GSAP and causing potentially undefined behavior:

 

const tl = gsap.timeline();
console.log(tl.totalProgress());

 

  • Thanks 1
Link to comment
Share on other sites

48 minutes ago, avancamp said:

Potentially related, this code will return NaN, which I think ends up propagating through several parts of GSAP and causing potentially undefined behavior:

What version of GSAP are you looking at? I can't seem to reproduce it in recent versions. 

Link to comment
Share on other sites

I have tested 3.1.1 and the 3.1.2 beta.

 

Here is a 3.1.1 codepen that reproduces this issue every time for me on Chrome and Firefox on two different computers: 

See the Pen jOPPLoX?editors=0010 by Lange (@Lange) on CodePen

  • Thanks 1
Link to comment
Share on other sites

Sorry, my local setup was using the 3.1.2 beta which seems to work fine even in your codepen. You're saying you get NaN with 3.1.2 as well?? I wonder if you had an old cached version loaded? 

 

In older versions, that would only happen if the totalDuration was 0 (very rare). 

Link to comment
Share on other sites

It is possible that I am somehow using an incorrect build of 3.1.2 beta. If you could send an npm-formatted zip of the latest build, I can try that in my real-world app and see if its behavior changes.

Link to comment
Share on other sites

1 hour ago, avancamp said:

It is possible that I am somehow using an incorrect build of 3.1.2 beta. If you could send an npm-formatted zip of the latest build, I can try that in my real-world app and see if its behavior changes.

Sure, here's a tarball file you can install: https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-beta.tgz

Link to comment
Share on other sites

I was indeed not on that exact build, but this new build seems strange in that the .parent property is being unset at unexpected times? Here's the minimal repro:

 

import { gsap } from '../node_modules/gsap/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);

class Foo {
	constructor() {
		this.timeline = gsap.timeline();
		console.log('parent immediately after construction', this.timeline.parent);
	}

	right() {
		this.timeline.to(header, {
			x: 300,
			duration: 1
		});
		return this.timeline;
	}

	left() {
		this.timeline.to(header, {
			x: 0,
			duration: 1
		});
		return this.timeline;
	}
}

const instance = new Foo();

// This call works.
instance.right();

// This delayed call does not.
// Of note is that the timeline no longer has a parent.
setTimeout(() => {
	const anim = instance.left();
	console.log('parent 1.5s later', anim.parent);
}, 1500);

I can't reproduce this in a CodePen using https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-latest-beta.min.js, maybe it's not the same build as that zip you linked.

  • Thanks 1
Link to comment
Share on other sites

Very interesting -- this new build fixes this bug for that minimal repro, but not for my real-world app, so there is some other condition under which this same bug (timeline not playing, parent being null unexpectedly) may happen. I'll keep trying to find another repro.

Link to comment
Share on other sites

Got it, here's an even more minimal repro. The issue seems to occur when a timeline is created, but doesn't have any children added to it for a while.

 

import { gsap } from './package/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);

const timeline = gsap.timeline();
console.log('parent immediately after construction', timeline.parent);

// This delayed anim never plays.
setTimeout(() => {
	timeline.to(header, {
		x: 300,
		duration: 1
	});
	console.log('parent 1.5s later', timeline.parent);
}, 1500);

 

  • Thanks 1
Link to comment
Share on other sites

Ah, got it - yeah, that's an edge case that'd only happen if the timeline had new things added to it after it had already completed AND had a duration of 0. Exceedingly rare :)  But it should be fixed in the latest beta. Same links as above. 

  • Like 1
Link to comment
Share on other sites

It may be rare in other codebases, but it is used heavily in mine haha. For whatever reason, it's a pattern I've found myself using extensively. Thanks for updating the build, will report back once I test it.

 

EDIT: Well, not "for whatever reason", the reason is that GSAP timelines are a great tool for creating long-running sequential queues of things, so I often have these timelines that live for the lifespan of the page which get things added to them when various events fire.

Link to comment
Share on other sites

Okay, that latest beta fixed all the new issues, thanks for that. I am now back on my original issue: .call being skipped.

 

I believe I have found a minimal repro. The problem is indeed caused by pause/resume. I believe the issue is that when progress(1) and/or timeScale(99) are used, increasingly large parts of the timeline are considered to be "under the playhead" in a given tick, and therefore more and more things get skipped when resume is invoked, due to it always setting suppressEvents = true.

 

This is a 3.1.1 CodePen, but the issue appears to be the same on 3.2.0-beta. Look at the console output to see if the test passed or failed:

 

EDIT: Also, if autoRemoveChildren: true is added to the timeline, it appears that onComplete gets called twice?

 

See the Pen xxGwOqY?editors=0010 by Lange (@Lange) on CodePen

 

Edited by avancamp
Discovered additional bug interaction with autoRemoveChildren
  • Thanks 1
Link to comment
Share on other sites

That's quite an interesting edge case - let me explain what was happening...

 

You've got a callback embedded in your timeline that has a pause() command inside it, and AFTER that you've got various other tweens/callbacks. So when you force the playhead to the very end with progress(1), it renders things in the proper order but as soon as it hits that callback that triggers a pause() (on the very timeline that's in the process of rendering), it halts the rendering process which is precisely what it should do. So the other stuff doesn't render. But the playhead is now all the way at the END (and it can't go any further). There's code in the rendering loop that basically says "if I'm asked to render at the same time as where the playhead currently is, just skip it because that's a waste of resources to do all that rendering when the playhead hasn't moved!" 

 

So when you resume() and the playhead starts moving again, it doesn't actually move because it's already at the end! That's why those remaining tweens/callbacks didn't render. It's like "hey, render at a progress of 1" and the timeline is like "dude, my playhead is already there...so I ain't gotta render anything."

 

It's a very tricky scenario actually because technically the playhead's position is only partially accurate (some of the children rendered, some didn't, because you paused part-way through a render). See what I mean? The best solution I could come up with is to sense that condition and shift the totalTime playhead back by 0.00000001. I think it's logically impossible to accommodate every aspect of this perfectly (the playhead can only be in one position after all). In my 12+ years of doing this, I don't think I've ever seen this scenario in a real-world project :) Congratulations, you're special. 

 

I've implemented this fix in the latest beta. Feel free to kick the tires. 

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

@GreenSock Thanks, I'll give it a whirl tomorrow morning and report back.

 

Also, if this edge case is just too strange to accommodate, I would accept something that prints a warning to the console advising against this behavior if GSAP detects this sort of thing. I don't mind changing my code, as long as I can't accidentally walk into this footgun again.

 

And yeah, broadcast graphics are super weird and require all kinds of weird uses of tooling haha. There's so many things I do that must seem absurd when viewed from a normal web dev perspective.

 

For example, I have graphics that ideally need to run for 7 full days straight. That's a very long-lived timeline, which is constantly reacting to events and queuing up animations. I also need to have systems which can immediately advance any animation to its end state, so that an operator can preview what the graphic will look like before they air it live. Stuff like that.

  • Like 1
Link to comment
Share on other sites

The issue has changed, but I don't think it is fixed. Here's another repro which adds autoRemoveChildren and re-orders the tweens/calls, and gets the issue to happen again. Of note is that if the final tween is commented out, the test passes. Very strange: 

 

import { gsap } from './package/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);

let called = false;
const timeline = gsap.timeline({
	autoRemoveChildren: true,
	onComplete() {
		report();
	}
});

timeline.call(async () => {
	timeline.pause();
	await waitForImageToLoad();
	timeline.resume();
});

timeline.call(() => {
	called = true;
});

timeline.to(header, {
	x: 300,
	duration: 1
});

// When this tween is commented out, the test passes.
timeline.to(header, {
	y: 100,
	duration: 1
});

timeline.progress(1);

function waitForImageToLoad() {
	return new Promise(resolve => {
		setTimeout(() => {
			resolve();
		}, 300);
	});
}

function report() {
	if (called) {
		console.log('Success!');
	} else {
		console.log('Failed, callback skipped.');
	}
}

 

  • Thanks 1
Link to comment
Share on other sites

Gosh, you're good at finding obscure edge cases! That's a good thing :)

 

I'll spare you the lengthy explanation about why this is so tricky. I believe that's fixed in the latest beta. Same links as above. Better? 

  • Haha 1
Link to comment
Share on other sites

1 hour ago, GreenSock said:

Gosh, you're good at finding obscure edge cases! That's a good thing :)

 

Haha, it comes from having used GSAP for about 6 years and having written many tens of thousands of lines of animation code with it. At this point I have a massive library of really complex graphics I can fire up which, for better or worse, strain parts of GSAP in ways they are perhaps not intended to be strained.

 

I have unfortunately found another repro, this one doesn't necessarily involve progress or timeScale either:

 

EDIT: Gosh I'm a fool, this repro makes no sense and will always fail lmao. Sorry, let me go try again...

 

 

Link to comment
Share on other sites

Okay, here's an actual repro that makes sense and won't waste your time lol:

 

import { gsap } from './package/index.js';

const header = document.createElement('h1');
header.textContent = 'Hello world.';
document.body.appendChild(header);
let called = false;

// On a timer because I'm not sure that putting this in
// an onComplete does the right thing in this test case.
setTimeout(report, 1000);

const parent = gsap.timeline();

parent.add(makeChild());

// Yes, this being delayed is part of the repro.
requestAnimationFrame(() => {
	parent.progress(1);
});

function makeChild() {
	const tl = gsap.timeline();

	tl.call(async () => {
		tl.pause();
		await waitForImageToLoad();
		tl.resume();
	});

	console.log('Creating grandchild timeline and adding it to the child timeline...');
	// This entire grandchild timeline is skipped.
	tl.add(makeGrandChild());

	return tl;
}

function makeGrandChild() {
	const tl = gsap.timeline();

	tl.to(header, {
		duration: 0.2,
		rotation: 15,
		x: 100,
		y: 150
	});

	tl.call(() => {
		called = true;
	});

	return tl;
}

function waitForImageToLoad() {
	return new Promise(resolve => {
		setTimeout(() => {
			resolve();
		}, 300);
	});
}

function report() {
	if (called) {
		console.log('Success!');
	} else {
		console.log('Failed, callback skipped.');
	}
}

 

  • Thanks 1
Link to comment
Share on other sites

Yay, another edge case :)

 

Should be resolved in the latest beta. 🎉

  • Like 1
Link to comment
Share on other sites

I think it might be fixed! I can't get the issue to repro on my real-world app anymore. I'll keep banging on it and report back if I manage to find any more issues.

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×