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

Rekapi integration #2

Open
bdowning opened this issue Apr 20, 2016 · 12 comments
Open

Rekapi integration #2

bdowning opened this issue Apr 20, 2016 · 12 comments

Comments

@bdowning
Copy link
Owner

@jeremyckahn, figured I talk about this here for now since it's not remotely ready.

This is what I had to do to use Retween instead of Shifty in Rekapi:

commit 5b9d2e0b46c771a507f98195e65528eed30287c9
Author: Brian Downing <bdowning@lavos.net>
Date:   Wed Apr 20 13:00:01 2016 -0500

    Retween

diff --git a/src/rekapi.core.js b/src/rekapi.core.js
index 5909f48..c8b723b 100644
--- a/src/rekapi.core.js
+++ b/src/rekapi.core.js
@@ -336,6 +336,15 @@ var rekapiCore = function (root, _, Tweenable) {
    */
   Rekapi._rendererInitHook = {};

+  Rekapi.easingFormulas = Object.assign({ }, Retween.easingFormulas);
+
+  Rekapi._retweenPreprocessor = Retween.composePreprocessors(
+    Retween.createEasingPreprocessor(Rekapi.easingFormulas),
+    Retween.createTokenPreprocessor(),
+    Retween.createColorPreprocessor()
+  );
+
+
   /**
    * Add an actor to the animation.  Decorates the added `actor` with a
    * reference to this `Rekapi` instance as `this.rekapi`.
diff --git a/src/rekapi.keyframe-property.js b/src/rekapi.keyframe-property.js
index 13ece79..c865a17 100644
--- a/src/rekapi.keyframe-property.js
+++ b/src/rekapi.keyframe-property.js
@@ -4,9 +4,7 @@ rekapiModules.push(function (context) {

   var DEFAULT_EASING = 'linear';
   var Rekapi = context.Rekapi;
-  var Tweenable = Rekapi.Tweenable;
   var _ = Rekapi._;
-  var interpolate = Tweenable.interpolate;

   /**
    * Represents an individual component of an actor's keyframe state.  In most
@@ -32,10 +30,23 @@ rekapiModules.push(function (context) {
     this.easing = opt_easing || DEFAULT_EASING;
     this.nextProperty = null;

+    this.regenerateTweening();
+
     return this;
   };
   var KeyframeProperty = Rekapi.KeyframeProperty;

+  KeyframeProperty.prototype.regenerateTweening = function () {
+    if (this.name !== 'function') {
+      var out = Rekapi._retweenPreprocessor({ v: this.value }, { v: this.easing });
+      this.retweenState = out[0];
+      this.retweenEasing = out[1];
+      this.retweenDecode = out[2];
+      this.retweenInterpolator =
+        Retween.createInterpolator(this.retweenState, this.retweenEasing);
+    }
+  }
+
   /**
    * Modify this `{{#crossLink "Rekapi.KeyframeProperty"}}{{/crossLink}}`.
    * @method modifyWith
@@ -55,6 +66,8 @@ rekapiModules.push(function (context) {
     }, this);

     _.extend(this, modifiedProperties);
+
+    this.regenerateTweening();
   };

   /**
@@ -75,25 +88,25 @@ rekapiModules.push(function (context) {
    * @return {number}
    */
   KeyframeProperty.prototype.getValueAt = function (millisecond) {
-    var fromObj = {};
-    var toObj = {};
     var value;
     var nextProperty = this.nextProperty;
     var correctedMillisecond = Math.max(millisecond, this.millisecond);

-    if (nextProperty) {
+    if (nextProperty && this.name !== 'function') {
       correctedMillisecond =
       Math.min(correctedMillisecond, nextProperty.millisecond);

-      fromObj[this.name] = this.value;
-      toObj[this.name] = nextProperty.value;
-
       var delta = nextProperty.millisecond - this.millisecond;
       var interpolatedPosition =
       (correctedMillisecond - this.millisecond) / delta;

-      value = interpolate(fromObj, toObj, interpolatedPosition,
-          nextProperty.easing)[this.name];
+      var interpolatedState = nextProperty.retweenInterpolator(
+        this.retweenState,
+        nextProperty.retweenState,
+        interpolatedPosition
+      );
+
+      value = this.retweenDecode(interpolatedState).v;
     } else {
       value = this.value;
     }

It is actually already faster at runtime, even without a fancy compiler. (It is much slower than Shifty at load time; I'm not doing any sort of caching yet.)

Shifty:
rekapi-shifty-profile

Retween:
rekapi-retween-profile

That is with a smidge over 64 actors (and a bunch of keyframes, something like 1300 per actor).

It actually even passes all the tests, except for the ones comparing tokenized CSS output (since I'm always printing them with two decimal places afterwards).

@jeremyckahn
Copy link

This is incredible! These are really significant performance improvements. Considering how much better Rekapi is already performing with Retween over Shifty, I am definitely in favor of swapping them once Retween is ready. Keep it up!

@bdowning
Copy link
Owner Author

Well, that escalated quickly.

2016-04-21-005432_1302x378_scrot

First of all, I discovered an issue with my test app such that I wasn't even rendering at 60fps. (Turns out that an audio element's currentTime attribute doesn't update that quickly.) So after fixing that I get a consistent framerate.

This is with a compiled interpolator and decoder (the bit that turns the interpolated values back into their source format, i.e. CSS). Further I modified Rekapi to lift the interpolation out of the KeyframeProperties and into the actor's timeline cache. This way you can update the whole actor in one interpolate and decode call.

Unfortunately there is more load-time processing, and I don't think there's anything I can do about it. However, I managed to hide it pretty well. Basically I'm just generating the Retween interpolators in a setTimeout(..., 0) chain. These seem to nicely get out of the way whenever requestAnimationFrame needs to run.

Here's a typical frame. At this point the granularity of Chrome's profiler is such that each frame's flamegraph is pretty random.

2016-04-21-010207_1304x376_scrot

And here's the bottom up (heavy) functions.

2016-04-21-010845_1303x373_scrot

I think it's a very good sign that (program) is getting 50% of the time. Disturbingly I think it's probably to the point where replacing Underscore/lodash iteration functions with real loops would probably be measurable (see lodash's forOwn taking 4% time for instance). Not that I think that's necessary, but it shows that stuff is pretty tight.

The profiles above were after it was done grinding through creating the Retween interpolators. Here's a flamegraph of a page reload:

2016-04-21-011506_1304x374_scrot

And here's a graph of the start of playback at about the same scale as the first image. Note the activity between the frames; that's Rekapi running ahead of itself compiling Retween keyframes.

2016-04-21-011603_1305x373_scrot

This is with 67ish actors and about 1300 keyframes per actor. I'm not using the setActive feature to deactivate any actors.

@bdowning
Copy link
Owner Author

(Annoyingly despite all that (idle) time Chrome still manages to consume 100% CPU time while the music/animation is running. 😠)

@bdowning
Copy link
Owner Author

(The changes are in the retween branch over in my Rekapi fork. They're an awful mess right now, it's just a total hack-job to get something working.)

@bdowning
Copy link
Owner Author

And nope, doing what I did with keyframes is not OK. I forgot that each property effectively has its own separate set of keyframes, so they do need to be tweened separately. Oh well.

@jeremyckahn
Copy link

This is a pretty interesting and thorough breakdown, thanks for laying it all out. It sounds like there's a non-trivial preprocessing cost for larger timelines like the ones you are working with, which some of your earlier work helped to mitigate. Do you think Rekapi is still faster at startup with Retween than before jeremyckahn/rekapi#45?

Do you think that the design of Rekapi needs to be reconsidered to accommodate Retween properly? And generally speaking, what do you think are advantages and disadvantages of using Retween instead of Shifty once the former is in a stable state?

@bdowning
Copy link
Owner Author

That's a good question. From digging around in various profiles I think I've come to an initial conclusion that if what you're animating is the DOM, the tweening engine performance really isn't that important; the DOM manipulation is so slow in comparison that it'll be lost in the noise. Figuring out and nailing the DOM manip to make it as fast as possible is what really needs to be done here. (Probably looking at what React and some of the other vdom engines do would be a good start.) I think batching Shifty to run in one timeout handler will help a lot here, since the browser should definitely wait to re-layout until that's done, whereas with individual timers it may layout multiple times.

For Canvas I think it's more relevant, and even more so for something like WebGL (though ideally you'd want to try to do your tweening in vertex shaders there, which is a whole different ballgame). Certainly Retween improved runtime performance in my case, though I also made some more improvements elsewhere. If I had to guess I'd say that the most major improvement was the compiled "decoders" to recombobulate CSS after tweening; you went to a lot of effort to sanify the output there which while good also takes a lot of time, whereas I just basically brute force concatenate strings together (and play some tricks like converting all rgb statements to use the percentage-based format where floating-point numbers are acceptible). I can go back and run some pre-Retween profiles, but I think that was the majority of the cost of my test. Also the Shifty "interpolate" addon which is used in Rekapi is kind of an end-run around the normal way Shifty works; that's actually what really sparked me to write this. It really hurts to have to allocate, partially configure an object, and run hooks just to tween some numbers once. The fact that you have to process the "css"-style tokenization input side every frame hurts even more.

The Retween startup cost is non-trivial. On the other hand the startup cost is O(keyframes), whereas the Shifty cost is O(frames), and for that cost you can do any input-side transformations (like "tokenizing" CSS) once per keyframe rather than once per frame. It really depends on the animation and the data being animated which one is going to come out ahead.

I'm going to forge ahead with using my hacked Rekapi for my project (I have to remember that I'm trying to light up LEDs here, and not get too distracted! 😄) Hopefully more experience will yield a better insight into this.

@bdowning
Copy link
Owner Author

bdowning commented Apr 22, 2016

Do you think Rekapi is still faster at startup with Retween than before jeremyckahn/rekapi#45?

Dramatically. I can go try to run it but there was enough O(keyframes^2) stuff I found in #45 and #46 that any long animations will basically be unusable no matter what tweening engine is used; you never make it to the point it will play at all.

@bdowning
Copy link
Owner Author

My test case I've been using recently (~64 actors, ~1300 keyframes each)

Rekapi w/Retween: 2sec (with further background processing ~10sec, not affecting playback)
After #46: 2sec
After #45: 4.5min
Before #45: >5min, gave up

Reduced to ~130 keyframes:

Rekapi w/Retween: 0.5sec (with further background processing ~3sec, not affecting playback)
After #46: 0.3sec
After #45: 3sec
Before #45: 80sec

Given that the non-lazy cache rebuild was definitely an O(n^2) thing, at 1300 keyframes it probably would have taken hours before #45. (You can see why I noticed it! 😃)

The big problem I fear with the background processing is that it is allocating garbage, so you could get GC pauses. I haven't seen them in mine so far, but when I bumped it to 640 actors I couldn't play it with it going on due to pausing. OTOH 640 actors wouldn't run at all on Rekapi/Shifty as it exists now...

Here's Rekapi/Shifty as of the active branch with the 64/1300 test:

2016-04-21-223324_1360x546_scrot

2016-04-21-223334_1360x546_scrot

And here's Rekapi/Retween, with some additional microoptimizations from my retween branch, which were minor enough they should be in the noise for Shifty (mostly replacing hot Underscore loops/iteration with native Javascript ones):

2016-04-21-223658_1363x547_scrot

2016-04-21-223710_1364x549_scrot

Rekapi/Shifty manages 60fps here, but there's not much headroom.

@jeremyckahn
Copy link

Wow, Retween really does make a huge difference. FWIW, Rekapi and Shifty were both designed to favor flexibility over performance while hopefully striking a decent balance between both. This suits the majority of use cases, but apparently not the ones that you are dealing with. That's why I'm excited about this project: It addresses the need for a higher level of performance.

Could you clarify your goals with Retween? Are you aiming to replace Shifty wholesale in my fork of Rekapi (which I would support), or are you planning to stick with your Retween-based fork of Rekapi? I understand if your fork deviates too far from mine such that it doesn't make sense to merge the two. However, it would be nice to pass along these performance gains to existing users of my fork when they upgrade.

Alternatively, would it make sense to support both Shifty and Retween as the interpolation engine in Rekapi, based on configuration? If so, would there still be a benefit to using Shifty over Retween if the API is the same in either case?

Based on what you've laid out here, it seems like Retween should replace Shifty, assuming that the API and usage of Rekapi doesn't have to change. I've put a lot of work into Shifty over the years, but I have no issue replacing it with Retween if that's a better solution to the same problems. I wrote Shifty to address my own needs, but this is why open source is so great: Someone can come along and improve something so that it works better for everyone!

@bdowning
Copy link
Owner Author

Right now my goals are primarily to be able to make progress on my lighting project; I've got to decide if it's going to be feasible to do it at all relatively soon, and then have enough lead time to buy lighting, assemble it, and program sequences before the event (in November, so it really needs to be working a ways before that). This is a volunteer gig too so I can't afford to dedicate 100% of my time to it.

As far as Retween is concerned, I would like to see it go somewhere, but since it's only existed for a couple days I'd be very reluctant to do anything like freeze the API now. I'd rather give it time to bake while I/others use it for a while to see if it remains stable. That being said, it's not like there's a whole bunch of API surface, and it's not terribly visible whether you're using this or Shifty in Rekapi other than performance, so if you're gung-ho to release a Retween-powered Rekapi I can just stick a 1 on the front of it; that's the nice thing about semver I guess!

@jeremyckahn
Copy link

I'm not pushing for any particular timeline, I'm just curious where you plan on taking this project. Take your time with it. I'll be around to help with any questions or PRs!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants