janestreet / incr_dom

A library for building dynamic webapps, using Js_of_ocaml.
MIT License
376 stars 32 forks source link

Question: Only update when a change has happened #15

Open wlitwin opened 3 years ago

wlitwin commented 3 years ago

Hi!

I've been using the Bonsai library for a side project and it's fantastic! The app runs on mobile as a single-page-application (SPA) and works well for the most part, except battery life. I've noticed that the battery drains fairly fast when using the SPA and after a bit of investigation I noticed a constant call to request_animation_frame.

It seems the source of this is that Incr_dom doesn't know when the computation graph is dirty, so it constantly tries to perform updates and stabilize the computation graph. I was wondering if there was a way to instead only call perform update when there are pending updates to the incremental graph?

I hacked it together for now here: https://github.com/wlitwin/incremental/commit/e54543805cb7c057a2f01d1e857df0d5efade9c0 https://github.com/wlitwin/incr_dom/commit/c489f81e8b4c73603e81aa78e9cc9ec3eec0665c

But I imagine there's a better way. I tried using Observer.on_update_exn on the top-level app, but it seems that will only fire during a stabilize.

Thank you!

TyOverby commented 3 years ago

Incremental should already know when the computation graph is dirty, and although request_animation_frame is called frequently, if there's nothing to do, it shouldn't do any work. Could you post your application code? Maybe you're setting an Incr.Var.t very frequently and that's causing the whole tree to be recomputed?

wlitwin commented 3 years ago

Unfortunately the application is somewhat large and dependent on a websocket server to do most of its work. But I'll try to come up with a smaller example over the coming weekend.

In terms of the frequent var setting, I don't think that was happening because the browser profiler showed no activity (except a minor GC every ~5 seconds) after the change. If there was a var being set I would expect the animation frames to keep happening even after the change, as the dirty callback would be called on var set.

I should also say this isn't a major issue, I was only curious if there was a kind of "push" API instead of a polling one for incremental/incr_dom. I guess in the normal usage the application would know when things have changed, but it's a bit harder in the browser environment.

Here's some Firefox profiles of doing the same action in the app with and without the change for comparison, but probably not that useful. profiles.zip

wlitwin commented 3 years ago

I figured out how to collect a system trace of the phone using adb, unfortunately my phone is too old to get some of the newer properties, but the trace shows pretty clearly that running request_animation_frame adds load to the system even though it does nothing. I collected these traces using the counters example from Bonsai with my modified incr_dom and an unmodified incr_dom. They are 10 second traces where every other app and tab on the phone are closed and the page is sitting idle in the foreground (no user input).

You can load the attached traces into the viewer here: https://ui.perfetto.dev/#!/viewer but I've added some screenshots below. The first screenshot shows the modified incr_dom and the second the unmodified:

counter_example_no_polling counter_example

To reproduce: 1) Install adb https://developer.android.com/studio/command-line/adb 2) Enable developer mode on the phone https://developer.android.com/studio/debug/dev-options 3) Setup the scenario on the phone, e.g. closing all apps and loading chrome with 1 tab of the Bonsai application. 4) Navigate to the android-sdk/platform-tools/systrace folder and run the following command: python2 systrace.py -o ~/counter.trace -a com.android.chrome gfx view sched freq idle load am wm view sync binder_driver input -t 10 5) Load the trace in the https://ui.perfetto.dev/#!/viewer viewer

Attachments: traces.zip

TyOverby commented 2 years ago

That's really strange! Looking at the traces, it seems like Chrome on Android is re-rendering the page on every frame even if nothing changed; you can see the chrome compositor doing a lot of work!

I'm not really sure that there's anything that Incr_dom / Bonsai can do about this. Checking to see if the graph is dirty is usually a very fast operation, and we need to do it for things like on_display and Incr.Clock.now to function.

wlitwin commented 2 years ago

Yeah, I'm not sure why it's so heavy. Desktop calls the same layout and composite methods, but spends far less time in them. As for the clock and on_display, the watch_now with the clock seems to still work well, and using it causes request_animation_frame to be called non-stop as normal. So the time example from Bonsai works as before. on_display from the documentation seems to not care about frequency, only that it is called right before a DOM change, so I think that's fine too (unless I'm misunderstanding its use).

But I was not taking into account a lot of the clock functions like at, after, and at_intervals, and have fixed those. This required exposing the next_alarm_fires_at for Incremental's timing_wheel. https://github.com/wlitwin/incremental/commit/fe7d16524ff48e6fd4818e2517581b000ff61ce8 https://github.com/wlitwin/incr_dom/commit/a2903b1c0989f8bbc2b563abf394c10f21596bdd

Anyway, I don't mind maintaining a fork, I'm probably the only one with this issue. Feel free to close this, and thank you for listening!

mooreryan commented 1 year ago

I'm seeing something similar to this as well with bonsai v0.15.1 (js_of_ocaml 5.1.1). Not sure if this is expected behavior or not, so I figured I would check. Here is an example of the type of thing getting run at every animation frame:

time

It isn't a lot of time spent in scripting in total (~2.5% of sample time, sample is the app idling), but still, it is interesting. (Of course it is apples to oranges, but a hello world-ish react app spends no time in scripting in a similar time sample.)

pie_chart

For reference, the bonsai code is this:

let component = Bonsai.const (Vdom.Node.text "hello bonsai world")

let (_ : _ Start.Handle.t) =
  Start.start Start.Result_spec.just_the_view ~bind_to_element_with_id:"app"
    component