This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-09-07
Channels
- # architecture (35)
- # babashka (9)
- # beginners (31)
- # biff (15)
- # calva (8)
- # catalyst (3)
- # cider (7)
- # clerk (4)
- # clj-kondo (24)
- # clj-yaml (10)
- # clojure (58)
- # clojure-europe (65)
- # clojure-japan (1)
- # clojure-nl (1)
- # clojure-norway (89)
- # clojure-spec (1)
- # clojure-sweden (1)
- # clojure-uk (8)
- # clojurescript (14)
- # cursive (3)
- # datahike (1)
- # datomic (29)
- # emacs (8)
- # graalvm (20)
- # graphql (1)
- # gratitude (2)
- # helix (6)
- # hyperfiddle (65)
- # jobs-discuss (7)
- # leiningen (1)
- # lsp (6)
- # malli (14)
- # missionary (12)
- # nrepl (8)
- # off-topic (24)
- # polylith (29)
- # reagent (14)
- # sci (14)
- # shadow-cljs (6)
- # spacemacs (10)
- # sql (4)
I have performance question, referring to https://cljdoc.org/d/reagent/reagent/1.2.0/doc/tutorials/-wip-managing-state-atoms-cursors-reactions-and-tracking: How cheap are Reactions? Is there a big penalty in performance for over using them? Is there a rule of thumb as to when the overhead outweighs the benefits? For example:
(def state (r/atom {:a {:b {:c 1}}}))
(r/cursor state [:a :b :c])
(-> state
(r/cursor [:a])
(r/cursor [:b])
(r/cursor [:c]))
Will the second option perform really badly compared to the first?
I'm trying to understand generically how I balance between caching reactions and deref + recompute?IMO that's a micro-optimization you shouldn't be worrying about. If your app is slow - profile and alleviate the bottle-neck. Cursors themselves aren't likely to ever be bottle-necks unless you have at least thousands of them.
Micro-optimization means use them as much as I want? or not use them at all because they don't matter?
That's exactly the point of the question, I'm trying to get a clue about when they practically make sense.
My point is that you shouldn't approach it from the perspective of performance of cursors themselves.
get-in
is cheap. cursor
is cheap. Re-rendering the whole component that needs only (get state :x)
just because someone else did (swap! state assoc :y 1)
is not cheap. This is where the performance question stands, not at whether or not you should use many cursors instead of a get-in
. At least, unless you prove otherwise with an actual profiler. ;)
And of course there's a design concern. Usually it's much better to feed some component only the state that it needs instead of feeding it everything and letting the component sieve through the data.
FWIW for simplicity of it all I'd use the (r/cursor state [:a :b :c])
variant.
Unless something else needs just :a
, then I'd use a combination of both approaches. In other words, create what needs to be created and reuse as much as possible.
I asked because making the second variant is simpler for me and if the cost isn't known to be high I'd try that. But if it's 3x more expensive than the standard variant then I'll just go for smarter more involved code.
(apply str ["a" "b" "c"])
will invariably take longer than (.join #js ["a" "b" "c"] "")
- which one would you choose?
Rendering of a component will trump the performance of a cursor.
If rendering of a component takes 20 ms and replacing one way to get your data with another gains you 0.1 ms - what's the point? Why even spend your brain's cycles to think about that?
(The issue is exacerbated if on all reasonable hardware your app is rendered in under 17 ms - then any talk of performance is completely useless.)
I just tested. The initial state is a map 3 levels deep with 10 keys at each level, so 1000 items in total. The first type of test was with a 1000 cursors with path of length 3, covering every value. The second type of test was with 10 cursors for the first level, 100 cursors for the second, 1000 for the third, all with paths of length 1. The second test was 10% faster. That's it. IMO not even worth pondering about.
Thank you 🙏 I think we'll start with derefing with no optimizations and see what happens
Actually, I realized that I wasn't comparing properly, without isolating the rendering out.
It's hard to isolate just the cursor stuff from everything else, so I focused only on what's going on in reagent.ratom
.
With that, nested cursors are faster than plain ones by 43%.
But it also goes to show even more that rendering of even the simplest of components (just a few spans and divs with strings and a sum of a thousand numbers, compared to 1000 cursors in one case and 1110 in the other) makes the performance of cursors pretty much irrelevant.
Actually you tested a case in favor of the cursor because you saved a lot of unnecessary propagation. But I wonder what the effect will be on a full scale application (which load time is not very fast as it is) where each access to composites in ratoms will create a cursor. Most of them are completely unnecessary.