facebook / yoga

Yoga is an embeddable layout engine targeting web standards.
https://yogalayout.dev/
MIT License
17.23k stars 1.42k forks source link

Question: performance of reusing layout object. #73

Closed mrjjwright closed 9 years ago

mrjjwright commented 9 years ago

I am writing a Mac app that use Layout.c wrapped in Obj-C with a React like component virtual rendering model. I am wondering how much performance is gained by reusing the same Layout object on subsequent layouts. I am trying to evaluate whether it's worth it to keep a stateful Layout.c backed object between renders or not. I notice there is logic in Layout.c that check dirty bits and previous dimensions and the like. Is the performance consideration substantial (e.g. akin to auto layout incremental calculations) or perhaps not worth it, compared to the ease of programming of not having to track adding, removing, renaming and updating of layout params on a stateful layout object? My own intuition is leaning towards keeping layout stateful.

vjeux commented 9 years ago

The very basic caching layer that I implemented gives a huge performance benefit for React Native workload.

1) Most updates in React Native are small, when you click a button, touch something or animate there's only a handful of elements that are affected after the diff algorithm. 2) We keep everything in a single layout tree, all the previous screens, all the ListView elements... Recomputing the layout for all that takes a non trivial amount of time.

By having the caching layer, we keep small updates super fast.

As another anecdote, right now ComponentKit on iOS doesn't have caching implemented and cannot do 60fps animations because layout is now their bottleneck.

mrjjwright commented 9 years ago

Thanks @vjeux for the lighting fast response as usual. :) Interesting feedback. It looks layout is recomputed everytime because node->isDirty is always true. How does your caching layer work at a high level? I also am designing this from the start to compute layout off the main thread and to not create unnecessary views that just do layout.

mrjjwright commented 9 years ago

Feel free to just point me to a file name in React Native and I will figure it out.

mrjjwright commented 9 years ago

I am leaning towards keeping a global layout map for all layout data like I do for component state and just keep track of all layout params as they change. Then pass this state off to a stateless Layout.c backed Layout object that recomputes only portions of the layout tree that have changed.

vjeux commented 9 years ago

Every time we update any property that impacts layout, we dirty the node and all the parents up to the root. https://github.com/facebook/react-native/blob/72d3d724a3a0c6bc46981efd0dad8f7f61121a47/React/Views/RCTShadowView.m#L271

Then, the next time we recompute layout, we start from the root and bail out super fast on all but the paths that have been dirtied. This general dirty strategy is really powerful, this is how we implement nested text, propagation of the background color and we want to use it for more in the future.

One optimization that we can do and should be safe is instead of going all the way up to the root, stop at any element that's position: absolute

vjeux commented 9 years ago

that recomputes only portions of the layout tree that have changed

The big difference between React component model and layout is that a React component CANNOT affect the state of the parent, but layout can.

Let me give you a super dumb example. You have a container with two children stacked vertically. If the first child updates and becomes taller, then you need to change 1) the parent dimension and 2) the second child position.

You have two options:

1) You can implement an algorithm that's going to try to track down the impact of all the updates and figure out the affected elements, but that's really hard to write and to get right.

2) You can just restart from the top and use a caching strategy that's going to bail out for things you know for sure cannot have changed.

mrjjwright commented 9 years ago

Oh great point, you just saved me from going down a bad path, that makes very good sense. Holy cow, thanks so much! Closing and off to some simple dirty checking .

vjeux commented 9 years ago

It took me almost a year to realize that! I've been trying to fit layout inside of the React model so many times and didn't understand why it wasn't working.

mrjjwright commented 9 years ago

So I decided to try to actually just use React Native's UIKit layer to drive my views. Hold your breath :) I want try to actually use React Native's native layer with my component layer since the React side already has so much work put into it and handles layout, updating of the views and everything else so well. From my component side I can pass NSArray, NSDictionary to the React side quite easily to mimic the necessary JSON. I don't want to use React as the component layer because I am experimenting with something I can't talk about quite yet. In 1 hour of work I am fairly close. :) I wrote my own RCTJavaScriptExecutor implementation to pretend like I am on the other side of the JS connection even though my app is running local. It's all working great in my own "javascript" thread. The only question is the protocol over the bridge going to involve anything crazy that you should warn me about right now? I am trying to reverse engineer it right now. Also are the Facebook police driving to Denver to arrest me right now?

vjeux commented 9 years ago

I wrote my own RCTJavaScriptExecutor implementation to pretend like I am on the other side of the JS connection even though my app is running local

Nice! At some point in the future we want to document that protocol so that we can have other abstractions on-top like Angular Native, Ember Native... If you conform to it, then you can use all the iOS and Android modules and API that have already been bridged.

mrjjwright commented 9 years ago

Cool so the approach I am using is looking through BatchedBridge, MessageQueue and of course looking at actually stuff sent from AwesomeProject. I should be able to figure it out.

On Apr 27, 2015, at 2:37 PM, Christopher Chedeau notifications@github.com wrote:

I wrote my own RCTJavaScriptExecutor implementation to pretend like I am on the other side of the JS connection even though my app is running local

Nice! At some point in the future we want to document that protocol so that we can have other abstractions on-top like Angular Native, Ember Native... If you conform to it, then you can use all the iOS and Android modules and API that have already been bridged.

— Reply to this email directly or view it on GitHub https://github.com/facebook/css-layout/issues/73#issuecomment-96812834.

mrjjwright commented 9 years ago

It's working!

On Apr 27, 2015, at 2:37 PM, Christopher Chedeau notifications@github.com wrote:

I wrote my own RCTJavaScriptExecutor implementation to pretend like I am on the other side of the JS connection even though my app is running local

Nice! At some point in the future we want to document that protocol so that we can have other abstractions on-top like Angular Native, Ember Native... If you conform to it, then you can use all the iOS and Android modules and API that have already been bridged.

— Reply to this email directly or view it on GitHub.

matt-d-rat commented 9 years ago

...and it was at this point the Facebook Police busted down @mrjjwright door and we never heard from him again ;-).

mrjjwright commented 9 years ago

Still alive here. :) I have a conceptual follow up question to this. I have succeeded in building a nice off the main thread layout system that uses a CADisplayLink (actually CVDisplayLink on Mac) to sync with the main thread and render.

However, if a root view (let's say a React Native root view or some other ui view hosting a layout system) has it bounds changed, obviously the layout needs to change. If the layout happens in a background thread, how to sync the changes visually with the root view? The root view will have it's frame/bounds change and by the time you calculate the layout on a background thread and get back to the main thread the user will see a jump visually. It's not that layout takes a long time, it's just that there is a break in the visual main thread transaction by switching to another thread.

I am seeing this when responding to AppKit NSWindow/NSView frame changed notifications by triggering layout, (there are visual gaps as the layout catches up with the user resizing the window). When I do everything on the main thread there is no jump. I looked through the React Native code and I didn't see any code that does any Core Animation transactions or anything like to make sure things are visually synced.

I guess having layout on a background thread works well for things that don't need immediate visual response but otherwise (for things like interactive animations) layout needs to be calculated immediately on the main thread? (Followup: this seems to be nicely explained here by the AsyncDisplayKit team. http://asyncdisplaykit.org/guide/4/. )

vjeux commented 9 years ago

You are spot on, we're facing this exact issue and haven't yet figured out the right solution to fix that :)

mrjjwright commented 9 years ago

I am thinking that designing for "async" layout first is good but a bit naive. In general a well designed UI rendering/layout system will be responsive on the main thread and give the user as much immediate visual feedback as possible. There is an obvious limit there (the each tick should take roughly 12ms limit) that can happen with text calculation or any number of things that take a long time, so the system must offload work asynchronously and bring the result of that work smoothly back in with the parallel work that has been occurring on the main thread for the user. In order for this to happen you have be able to reason about the animation, layout and measurement work along these lines.

I am wondering if this same sort of reasoning ability should be a first class citizen in a well designed component system as well. The component author needs to be able to reason which of their components need to be immediately responsive and which can have work (and it's associated props and state) offloaded to a background thread and then have the results smoothly brought in with main thread work.