Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
jchristof

CSSPlugin holding a reference to a removed html element

Recommended Posts

We've got a single page app here using TweenMax. Taking a look at the heap profile I see in my simple example that CSSPlugin is holding a reference to an html element that was removed from the dom. Could someone point me to documentation on how to release any held elements w/o breaking any follow up usages of the same TweenMax instance?

post-47279-0-49209900-1484776332_thumb.jpg

post-47279-0-70821400-1484776515_thumb.jpg

See the Pen XppvwE by jchristof (@jchristof) on CodePen

Link to post
Share on other sites

You can't really re-target a tween (swap out its target for something else). I know you're trying to optimize performance by reusing an instance, but that'd actually deliver WORSE performance because in order to be as fast and efficient as possible, GSAP records values internally based on the target, so it'd have to scrub all of that clean before re-recording new info for a new target. It'd be faster to just let that old one die/expire, and create a new tween instance when you need one for a new target. 

 

Does that answer your question? 

  • Like 1
Link to post
Share on other sites

No, in our case elements that are not garbage collected (like this one in the CSSPlugin) after they are removed from the dom prevent the garbage collector from running on our removed controls and unused app instances (references from the element to those items.)

 

Ultimately what I'd like is for TweenMax, CSSPlugin etc. to not hold an element past it's being removed from the dom. I don't mind calling a function to do this.

 

TweenMax.releaseDetachedDomNodes();

 

Multiples of our same app can run at the same time, so tearing down the whole TweenMax is not an option since one app may be running animations at the same time another is being unloaded.

 

Somehow grouping tween functionality might also work for us if I could say:

TweenMax.releaseEverthing(relatedToApp1);

and then later

TweenMax.releaseEverthing(relatedToApp2);

  • Like 1
Link to post
Share on other sites

I'm a little confused - you want to keep the tween (which specifically animates a particular element), but you don't want that tween to reference the target element (when you tell it not to)? You want to be able to rip out all the references inside the tween...but then when you go to reuse that tween instance, what do you expect will happen? How will it know what to tween, what the starting values should be, etc.? Are you saying you want to then feed in that reference again and have the tween go through all of its internal recordings and re-populate all that data? 

 

And you said "...so tearing down the whole TweenMax is not an option" - are you saying that's because the tween targets multiple objects, some that are in the DOM and some that aren't (anymore)? 

 

It may not be useful here, but do you know about the TweenLite.killTweensOf() method? 

  • Like 1
Link to post
Share on other sites

Sorry, let me rephrase that and go back to my original post and an example:

 

I create a div attached to the dom.

I use TweenMax.set() to position the div.

I call remove() on the div - it is no longer rendered by the page.

The div remains allocated and appears in heap profile.

The div is rooted to window.CSSPlugn.

 

I was not expecting gsap to keep a reference to this element. I expected the element to be reclaimed by the garbage collector after removing it.

Is there a way to ensure that TweenMax does not cache elements after they are removed from the dom?

 

For us, a node in that state produces unexpected side-effects that will be tricky to work around.

  • Like 1
Link to post
Share on other sites

Maybe it'd help if I explain how it works in GSAP...

 

When a tween renders for the first time (which is immediately with set()), it records starting/ending values for the target, and of course it must reference the target as well. Some of that is done in CSSPlugin if you're affecting CSS-related properties. Internally, GSAP maintains a lookup table that makes it super fast to find the tweens of a particular target (for overwriting). When the tween is done, it will flag itself as being eligible for garbage collection. Roughly every 2 seconds or so, GSAP clears out that lookup table of gc-eligible tweens. It's up to the browser to dump it after that, but GSAP removes its internal references to it (unless it's in a timeline, of course, which hasn't finished yet either). 

 

Are you maintaining a reference to the TweenMax instance? I assume so (your original post indicated that you want to reuse it, so you must be hanging on to it). If so, it only makes sense that it wouldn't release references to that DOM element. Otherwise, how would it work when you press that tween back into service, like tween.play(0.5)?

 

In order for TweenMax to automatically kill its own tweens and references of elements that aren't in the DOM anymore, we'd have to add performance-sucking code that'd constantly check that stuff (loop through every target of every tween...."are you still in the DOM?"). See what I mean? I don't think that'd be a good thing. If you want to kill the tweens of a particular element, that's totally doable with TweenLite.killTweensOf(). 

 

Does any of that help? 

  • Like 3
Link to post
Share on other sites

Great explanation of how GSAP handles garbage collection Jack, very informative Good Sir :)

Link to post
Share on other sites

Our uses of TweenMax all ignore the return object and use the static form. We don't new Tween() anythere.

 

In the case above there are several set() calls on the element:

TweenMax.set(htmlElement as any, { x: viewModel.left, y: viewModel.top, width: viewModel.width, height: viewModel.height, background: viewModel.background, position: 'absolute' });

TweenMax.set(this.domElement as any, {x: value});

TweenMax.set(this.domElement as any, { y: value });

TweenMax.set(this.domElement as any, { width: value });

TweenMax.set(this.domElement as any, { height: value });

 

None take the return value.

 

I have tried TweenMax.killAll() to try removing the window.CSSPlugin cached element.

Link to post
Share on other sites

Ah, okay, then things should automatically be released for GC within a relatively short time (typically a few seconds, though the browser decides when it wants to do the actual sweep). 

 

I think what was confusing was in your original post, you said "...w/o breaking any follow up usages of the same TweenMax instance" which sounds like you were keeping a reference of an instance (to reuse). 

Link to post
Share on other sites

We've got a single page app here using TweenMax...

 

That is the first thing that stood out to me. Are you using a framework? A lot of SPA frameworks require you call some function to let the app know when the animation is complete so that the element can be safely removed from the DOM. If that call is never made, the app might be keeping a reference to that element indefinitely, maybe inside some unresolved promise. 

  • Like 3
Link to post
Share on other sites

So going back to this example, if I run a heap profile 30 min. later, the window.CSSPlugin still has a handle to that element. What can I do manually to safely clear or reset it so that the CSSPlugin will release that element? I don't want to do anything that would compromise other running animations.

Link to post
Share on other sites

@OSUblake

It's our own framework that runs as an application in the page. The important part of the spa-ness in this instance is that the application page is required to run indefinitely and should never need a frame refresh to manage memory.

Link to post
Share on other sites

One other note - in order to function correctly we need to remove parts of the dom tree anytime a user elects to change part of the view - whether or not animations are alive or influencing any of those elements.

Link to post
Share on other sites

It definitely sounds like something on your end. Can you reproduce this issue outside of your framework?

Link to post
Share on other sites

See the Pen XppvwE by jchristof (@jchristof) on CodePen

 

<div id="dontholdme"></div>

 

TweenMax.set('#dontholdme', { x: 0, y: 0, width: 100, height: 100, background: '#ff0000', position: 'absolute' });
 
setTimeout(()=>{document.getElementById("dontholdme").remove()}, 1000);
 
Then sometime in the future open chrome devtools->profiles->take heap snapshot.
 
See that the window is holding a ref to CSSPlugin which is holding a ref to the disconnected dom element keeping it alive in memory.

post-47279-0-44259000-1484845205_thumb.jpg

post-47279-0-82263700-1484845205_thumb.jpg

  • Like 1
Link to post
Share on other sites

Hi. I not advanced developer nor developer. I am just fan of JS. I know and use GSAP since 2011 and always GSAP keeps boss place of animation engine and there not only Jack. There much of developers worked to be GSAP best of engines of animation. As i know heap memory, Garbage collection cannot be managed. No one browser guarantes you best performance even if you did garbaged all of objects and etc.

 

There my mind as fan.

 

1. Real performance differs from tests.

2. Make 100arrays inside loop and make 1arrays outside loop. This sounds like second test should be 100times faster. You get less memory heap, but for just 50 DOM elements testing animation with repeat you can test, you get just 3-5fps performance improvements.

3. Dont trust everything, test yourself.

4. Ask help, if they dont know, give example, so all should see what issue

  • Like 2
Link to post
Share on other sites

Ah yes, I think I know what's happening - for performance reasons, CSSPlugin just stores ONE variable that points to the most-recent element whose CSS-related values were animated. It's replaced every time. It does **not** store a reference to every element tweened (ever). That'd explain the behavior you described. In terms of memory management, it would never cause a buildup because it's constantly swapped out. But yes, it does point to a single element (the most recently animated one). If that's a problem for you, you could just make it point to something else by doing a CSS tween on a different element, even one that doesn't change anything, like:

TweenMax.set(otherElement, {x:"+=0"}); //does nothing

Does that help?

  • Like 2
Link to post
Share on other sites

Crazy! If a single element was that source of this issue, I just have to ask, how on earth did you even notice it to begin with? That's a lot of stuff to dig through.

Link to post
Share on other sites

I'll explain that in a second.

 

However, I've immediately discovered another issue. The following now appears in the heap profile after running animations. 

Again, I've ensured we've released all references to our application (appContext in the picture) but something called target is preventing our app from being gc'd:

 

 TweenLIte._internals.tweenLookup.t59.target.(my control).(my application)

post-47279-0-11340800-1484857836_thumb.jpg

Link to post
Share on other sites

That looks like the lookup table that gets cleared out a few seconds after the last tween on that element completes (or is killed). Nothing should stay in there forever. I wonder if you just didn't wait long enough for it to get cleared out before taking your snapshot. (?)

Link to post
Share on other sites

I'll explain that in a second.

 

However, I've immediately discovered another issue. The following now appears in the heap profile after running animations. 

Again, I've ensured we've released all references to our application (appContext in the picture) but something called target is preventing our app from being gc'd:

 

 TweenLIte._internals.tweenLookup.t59.target.(my control).(my application)

I am interesting of how this issue slowing down your site?

Is this really slow down responsiveness of page?

I using GSAP and i knew about Chrome DevTools just few month ago and it helped me a lot, but i again remember, REAL performance is differs from TESTS and Memory usage.

If you deeply care about performance, i recommend you "chrome://tracing/" as some peoples told how it helped him.

If you have issues about keeping element that removed, i recommend to use some below code.

 

EDIT: Micro-optimization doesn't let you what you want. So you should not care about micro-optimization

var target = document.getElementById('#myTarget'); // 1. getById is faster than others

var varsMap = new Set(); // 2. "Set" creates less memory than objects
var varsMap2 = new Set(); // #2

varsMap.x = 0;
varsMap2.x = 200;

varsMap2.onUpdate = function () {
 // your update here
 // like target.style.left = varsMap.x + "px";
};

varsMap2.onComplete = function () {
 target = null; // garbage it
 varsMap = null; // its too
 varsMap2 = null // its too
// when onComplete is called then tween is removed from tweens list
// target nowhere stores in TweenMax
}

TweenMax.to(varsMap, 2, varsMap2);


Link to post
Share on other sites

@Jack

I can faithfully reproduce it and our app remains allocated indefinitely by it's reference from tweenlookup:

 

TweenLite._internals.tweenlookup.t104.target.children[0].imageToBase64.context._this3.appContext.

 

This time I'm animating properties on plain-old-objects, and dom elements. I haven't had time to pull together a simple repro but generally:

 

Start animation on object properties

Call TweenMax.killAll()

 

setTimeout(()=>{ //wrap in settimeout or not - doesn't make a difference

    Shut down our app

    In-flight tweens update and complete - the bodies of onupdate() and oncomplete are wrapped in try/catch

 

},500);

 

No exceptions in the console log.

 

Just now I removed the try/catches from the body of onupdate and oncomplete and am able to crash chrome on a heap profile if I allow exceptions to bubble up into the TweenMax dispatcher for those functions [dispatcher exception jpg]

 

I guess for the moment is there at least a safe way for me to free that tweenlookup that won't interrupt other animations running in TweenMax? I can't leak an appContext.

post-47279-0-05968400-1484899801_thumb.jpg

post-47279-0-35298700-1484899801_thumb.jpg

Link to post
Share on other sites

A few thoughts:

  1. I mentioned it can take up to several seconds after the last tween on an element completes before it's removed from that lookup, but you set your timeout to 0.5 seconds. Maybe try waiting longer to check?
  2. It looks like you've got something in an onUpdate that's attempting to reference a property of a null object (unrelated to GSAP). Perhaps that's halting execution, thus freezing things where they were (and the lookup isn't emptied). 
  3. There certainly could be a bug somewhere, but it's very difficult to troubleshoot blind and we've had quite a few GC experts look at GSAP and confirm that it's rock solid. Plus yours is the only report we've gotten about anything like this (that I can remember), despite GSAP being used on over 2.5 million sites, vetted (and endorsed) by Google and all the major ad networks, used by most of the world's biggest brands, etc. I'm not saying that means there's not a bug - I'm just surprised that if there was some underlying GC issue someone else hasn't brought it to our attention. 
  4. Again, it'd be really, really helpful if you could provide a reduced test case that can run in a browser and isn't dependent on your framework. Possible? Perhaps there's a very particular context in which this happens. 
Link to post
Share on other sites

He there, I have the same problem, with this tweenLookup map, but I have an example of how to reproduce the issue. The example is pretty unnatural because it came from a separate project, but it is pretty simple. So the retained object is from the Slide class. If you do comment over the line: TweenMax.killTweensOf(holder), the object is successfully gc, otherwise not. 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>GASP memory leak</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenMax.min.js"></script>
    <script src="http://pixijs.download/release/pixi.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script>
        window.onload = function () {
            // starts rendering
            const renderer = PIXI.autoDetectRenderer(500, 500);
            $('canvas').append(renderer.view);
            renderer.backgroundColor = 0x000000;
            const stage = new PIXI.Container();
            animate()

            function animate() {
                renderer.render(stage);
                requestAnimationFrame(function () {
                    animate()
                });
            }

            // end

            // Define api here
            class Slide extends PIXI.Container {
                constructor(id) {
                    super()
                    this.id = id
                }
            }

            PIXI.DisplayObject.prototype.preorder = function (callback) {
                const length = this.children.length
                for (var i = 0; i < length; i++) {
                    if (this.children[i].hasOwnProperty('preorder'))
                        this.children[i].preorder(callback)
                    else callback(this.children[i])
                }
                callback(this)
            }

            // end

            // Define the Dsiplay List wich causes the memory leak - Slide object stands forever
            const navigation = new PIXI.Container()
            const container = new PIXI.Container()
            const holder = new PIXI.Container()
            const slide = new Slide(555)
            navigation.addChild(container)
            container.addChild(holder)
            holder.addChild(slide)
            stage.addChild(navigation)
            setTimeout(function () {
                stage.removeChild(navigation);
                TweenMax.killTweensOf(holder); // If you comment this line the Slide object will be collected from memory
                // destroy the navigation without it's textures
                navigation.preorder(function (child) {
                    if (child instanceof PIXI.Texture)
                        child.destroy(false);
                    else if (child instanceof PIXI.Sprite)
                        child.destroy(false, false);
                    else child.destroy();
                });
            }, 500)
            // end
        }
    </script>
</head>
<body>
<canvas></canvas>
</body>
</html>
Link to post
Share on other sites

Hm, you don't even have any animation code at all in there. Oddly enough, I think that's the problem. You see, as a performance optimization, GSAP powers down its ticker after a short time of inactivity (animation-wise). That ticker is what triggers the render cycles, of course, which are what trigger the clearing out of that tweenLookup object (when appropriate). So if you don't ever have any animations happening, and yet you ask TweenMax to kill tweens of a particular object, it registers that object and looks up any tweens (to kill), but since nothing ever renders again in your example, it doesn't run that clear-out logic. Extremely uncommon, but possible. Did you notice if the problem goes away if you have even one tween? Another solutions is to simply call TweenLite.render() to trigger that or TweenLite.ticker.wake(). I'll look into adding some conditional logic to skip the lookup/registration when you try killing tweens of an object that has never been tweened before. 

  • Like 1
Link to post
Share on other sites
Guest
This topic is now closed to further replies.

  • Recently Browsing   0 members

    No registered users viewing this page.

×