hoplon / javelin

Spreadsheet-like dataflow programming in ClojureScript.
803 stars 44 forks source link

Application slow on Ipad #12

Closed RSA199 closed 10 years ago

RSA199 commented 10 years ago

I use to write applications Javelin and found a problem: dom element moves pretty slow, jerky. Code:

(defc mouse-x 0)
(defc mouse-y 0)

;;...

(.addEventListener (by-id "my-dom-element") "touchmove"
                     (fn [event]
                       (.preventDefault event)
                       (let [touches (get (js->clj (.-targetTouches event)) "0")]
                         (reset! mouse-x (get touches "clientX"))
                         (reset! mouse-y (get touches "clientY")))
                       (update-new-my-dom-element-position!))
                       false)

Rewriting the code for no-frp-style (using atoms) - work was quickly. Then I did a benchmark:

(for [x 1000]
    (reset! cell-instance x))

time: ~300msec

(for [x 1000]
    (reset! atom-instance x))

time: ~8msec

What do you think, for this task is not suitable Javelin or i do something wrong?

micha commented 10 years ago

This task is probably not really suitable for Javelin. Javelin cells are best utilized in the application state graph, where you are likely to have parts that must be coordinated without necessarily knowing about each other. The consistency guarantee provided by Javelin is very useful there. But when there are no dependencies between things, like in your example, there is no need for such guarantees and regular atoms are appropriate.

I'd probably use a hybrid approach in your case, assuming that you had some greater goal above and beyond just moving an element around the screen. Like if you were animating a drag and drop widget, your application only cares about which dropzone the element is in, so that would be where you'd use Javelin cells. When the user drops the element into a new dropzone it would fire an event, which you could then wire up to a Javelin cell.

There are already tons of libraries that can handle animations, but Javelin gives you a coherent state-machine sort of graph to which you can attach them.

(time
  (doall
    (for [x (range 0 10000)]
      (reset! c1 x))))

;; :optimizations :whitespace
;; Elapsed time: 567 msecs

;; :optimizations :advanced
;; Elapsed time: 278 msecs
thedavidmeister commented 6 years ago

Unfortunately this old and isolated thread has been referenced multiple times in slack and other forums that I have seen as a hand-wavey "hoplon/javelin has unresolved performance problems so don't use it" kind of way.

I'd like to add some context for the next person to stumble along here.

what does the benchmark represent?

the benchmark from the OP is resetting a cell 1000 times and presenting it as indicative of something

this is very abnormal even for complex applications as typically, even if an application had 1000 or even 10000 cells they would not all be chained together in a single graph, rather you would have 100's or 1000's of smaller chains that each do a handful of calculations each.

updating the DOM 1000 times in a short period is inadvisable regardless of the framework (see below) or "scale" of the application.

does the existence of atom detract from cell?

the comparison presented by the OP seems akin to the volatile! vs atom! discussion https://clojuredocs.org/clojure.core/volatile!

Volatiles are meant to hold state in stateful transducers, since they are more performant (and less capable) than atoms.

In a very similar way, atoms are more performant and less capable than cells. Similarly, the existence of atoms doesn't detract from the usefulness of cells, just as volatiles do not detract from atoms, they complement them and make atoms more useful by offering a critical escape hatch for edge-cases.

what is missing from the benchmark?

the benchmark is flawed because the intent behind cells is that every step of the cell graph evaluates a function. Whatever functions are being called along the way in a normal app are going to be significantly heavier on CPU than the propagation itself, I have confirmed this anecdotally with different functions over multiple years using the in-browser profiling tools rather than a contrived benchmark. Remember that you don't reset! a cell 1000 times in situ, you reset! a single cell, and if it were to hypothetically propagate to 1000 other cells (very unlikely, see above) then you have also called 1000 arbitrary functions along the way, which is going to be much slower than the overhead of 1000 propagations.

there's no indication from the benchmark as to whether reset! called on a single cell repeatedly has any relevance to the performance of propagation of 1000 cell values along the graph after a single reset!.

even with a cell graph of 1000 cells, javelin performs an equality check at each step, if a calculated value is identical before and after it immediately halts propagation. This is common in realistic setups, so even complex graphs often only calculate and propagate a relatively small number of values. No simple benchmark can simulate this, so we have to rely on profiling and anecdotal evidence, which is typically quite positive (in slack, etc.)

how is the DOM being updated?

we see in the example a mystery function call (update-new-my-dom-element-position!) which presumably updates something in the DOM. There is nothing wrong with this but, in the context of criticism against Hoplon pointing to this thread:

is there an easy way to mitigate these issues?

the example shows eager evaluation of an event bound to touchmove, a recipe for jank with or without javelin... http://jankfree.org/

i suspect that a simple throttle/debounce on the logic could have avoided or mitigated the problems the OP was seeing (https://github.com/hoplon/hoplon/wiki/Performance#eagerness), but there is no way to know now without being able to inspect the entire application, including what formula cells may have been dangling off mouse-x and mouse-y and whether the "new and improved" atom implementation was comparing apples to apples here.

what do we mean by "this is a task not suited to javelin"?

in the case that we:

then cells are not really suited for such a task (as explained above), but this does NOT imply cells are inappropriate for the entire application, just an isolated task within the application.

what is the alternative?

So let's imagine that we do need to thrash 1000 cells eagerly on a touchmove event, and we assume that each of those 1000 cells has its own cell graph to propagate to... this is a lot of wiring to be handling by hand (https://adzerk.com/blog/2015/06/splint-functional-first-aid-jquery/).

The sheer volume and complexity of what 1000 co-ordinated/synchronous cell graphs would represent in LOC terms is mind boggling, let alone dangling this much logic off a single event handler. Once you get to this point of complexity then hand wiring or many other "frameworked" solutions (including react, etc.) are likely also infeasible too, for a host of other reasons!

is this benchmark still relevant?

it's really hard to say... browsers, javelin and hoplon have all changed significantly since 2014. I might try to reproduce the raw benchmark timings on the latest chrome/javelin, but i'm certainly skeptical about the relevance of all these numbers either way.

thedavidmeister commented 6 years ago

as i suspected (at least when tested on my humble 2012 model laptop), the difference today between javelin cells and atoms for a simple reset! seems much less than it might have been in 2014:

(let [c (j/cell nil)]
 (time
   (doall
     (for [x (range 0 1000)]
       (reset! c x)))))

(let [a (atom nil)]
 (time
   (doall
     (for [x (range 0 1000)]
       (reset! a x)))))

; "Elapsed time: 9.330000 msecs"
; "Elapsed time: 3.605000 msecs"

https://github.com/thedavidmeister/javelin-benchmark

i still don't think this benchmark is meaningful though ;)