Fork me on GitHub
#re-frame
<
2021-02-28
>
thheller09:02:31

has anyone ever implemented this benchmark for re-frame? https://github.com/krausest/js-framework-benchmark

thheller09:02:08

I'm kinda interested to see how the subscribe stuff performs in benchmark conditions

thheller09:02:55

not interested in the rendering/react aspects at all but I'm interested to see what the performance of mounting/unmounting queries looks like

ribelo09:02:06

@jacek.schae updates A RealWorld Comparison of Front-End Frameworks every year, and may have done other benchmarks, or would like to.

thheller09:02:53

yeah I saw that one but I was looking specifically for this benchmark

thheller09:02:16

mostly because its an easy setup to see some common situations in webapps in benchmark conditions

ribelo09:02:19

same thing only newer

thheller09:02:38

(eg. no app should have 1000 items rendered in a list but in a benchmark its useful to see)

thheller09:02:33

the real world bench is useful for other stuff and probably more useful overall but I'm specifically looking for examples that stress the subscribe mechanisms

thheller09:02:52

I can set write it myself, just wondering if someone else did before me

ribelo10:02:21

shouldn't the results be essentially identical to the reagent?

thheller10:02:21

the rendering yes but thats not what I'm interested in

thheller10:02:05

FWIW I did this benchmark for the stuff I'm working on and have two variants. one that would be similar to re-frame (full) and the other pure reagent (light). both perform more or less the same overall but full performs better in updates (eg. select row) but light performs better in mount/unmount (create/clear rows). I would expect re-frame to be somewhat similar in results when compared to reagent. https://github.com/thheller/js-framework-shadow-grove

mikethompson10:02:39

At some point, I'll free re-frame subscriptions from using reagent reactions

thheller10:02:37

that would be neato. then I could just plug it into my stuff šŸ™‚

mikethompson10:02:04

I'll be moving to using one map for all the subscriptions. The keys in this map will be the subscription vectors And the values in the map will be a map representing the subscription, including a dirty? flag, a set of dependent (subscriptions, identified by their subscription vectors) s, etc

thheller10:02:54

yeah. as far as I understand one "common" bottleneck for re-frame apps is when one modification results in too many subscription updates

thheller10:02:28

eg. in a map of a thousand items you update one and 1000 subscriptions run to figure out if they are dirty. which is fast as long as its just a (get db thing) and compares identical? for 999 items

thheller10:02:19

but if you actually compute something that becomes a bottleneck

mikethompson10:02:47

We've never found it to be much of an issue

mikethompson10:02:06

Because the identical? test shortcuts early

mikethompson10:02:15

And because we don't display 1000 things (elements of a vector?)

mikethompson10:02:40

My proposed rework of subscriptions is more about clean up and doing it a much simpler way

mikethompson10:02:56

Hey, while you are here ... I know you have been working on a framework ...

mikethompson10:02:33

... Zach Oaks has convinced me that using a map for app-db is ultimaitely a bad idea ... flat data is better

thheller10:02:34

maybe the stuff I tried to describe here with transacted observed would also benefit the re-frame model (it should) https://github.com/thheller/shadow-experiments/blob/master/doc/what-the-heck-just-happened.md

thheller10:02:30

I assume you are referring to his o'doyle rules thing?

thheller10:02:01

yeah that looks very interesting but seems impossible to scale this to what an webapp would need (adding/removing rules seems rather expensive)

mikethompson10:02:04

That was the reason I didn't use DataScript originally. We had too much data. And it took too long to import. But ... The problem with using a map is that it is very "placeful"

mikethompson10:02:36

I didn't realise quite how placeful until I started writing a tutorial on writing reusable components for re-frame

mikethompson10:02:49

It struck me a little hard. The difficult part of creating a reusable component in re-frame is that the component's subscriptions and event handlers have to know "where" in pp-db they need to access

mikethompson10:02:13

That means it is hard to take components from one app to another

mikethompson10:02:25

There are things you can do to mitigate this

mikethompson10:02:37

You can load data into well known places within app-db

thheller10:02:45

well you can always make things reusable by separating that out and just having a render function that takes the data and renders it

thheller10:02:07

and the other one that gets it from wherever and calls it

mikethompson10:02:31

These are all suggestions I came up with. BUT ... I was quite struck by this placefulness thing It means the subscriptions and event handlers had to get parameterised (by place)

thheller10:02:32

but I agree that flat is better. my normalized db thingy is one big map with one level of nesting.

thheller10:02:49

yeah but how else would you solve that?

mikethompson10:02:49

Anyway ... just a thought

mikethompson10:02:04

Datascript is flat

thheller10:02:27

but then you have to pay everwhere for turning it into maps so you can actually work with the data again

thheller10:02:29

(update thing :foo inc) becomes rather difficult too

mikethompson10:02:04

There were some libraries which stored "entities" in app-db in a very regular way. I can't find them offhand.

mikethompson10:02:40

They might yet be a middle ground

mikethompson10:02:43

Anyway, I don't have an answer yet, but it is on my mind.

thheller10:02:55

yeah I do think so. normalizing the data to one level to avoid duplication of entities seems to be enough. making it flatter to EAV tuples means you need to reconstruct maps all the time where you really just want to do (get db id) (which datascript has a wrapper for to emulate but thats not the same thing)

p-himik12:02:44

Just some potentially useful musings. I ended up rewriting subgraph to accommodate my needs and I'm still quite happy with the outcome. Although, I use it only for the domain data that comes from an RDBMS in an already normalized form. One thing I have found quite helpful is having multiple indices for every foreign key attribute and being able to query based on that. "Give me all items by a coll of IDs" is useful, but sometimes "give me all items that reference that other item" is even better.

p-himik12:02:20

Another feature that's very useful for my use cases is to be able to store user-caused changes separately and query the changed state and the original state separately. Sort of like a transaction. It facilitation creation of complex and reusable components where you can enter a bunch of data, see what exactly you've changed, check validation results, and only then save the data.

mikethompson10:02:52

Hmm. I'll have to read up again.

thheller10:02:58

ah that looks good

ribelo10:02:38

currently it looks like everyone is trying to solve the same problem in a similar way

thheller10:02:24

oh thanks. didn't see that one yet.

lilactown17:02:29

btw I'm basically just waiting for someone to ask for custom schemas in autonormal. happy to riff on what a good API for that would look like if anyone is interested

ribelo10:02:00

I am currently trying to solve the same problem myself as a hobby ; )

ribelo10:02:16

in production we use rum + patched datascript, but it is very slow and tedious

roman01la11:02:11

I think I'm struggling with a similar problem atm, where one thing in a collection changes and that causes recalculations in multiple subscription chains

roman01la11:02:26

Data is preprocessed first, then indexed in various ways and postprocessed at leafs. We have something that implements incremental indexing at the root, but further change propagation requires calculations from scratch in every subscription. My current idea is to have "streaming subscriptions" that would propagate changes only, similar to the idea behind transducers

ribelo11:02:55

The normalized flat db is great, but there is a problem similar to datascript, all subscriptions are recalculated with every change in the db. It is possible to do something like in posh, check each transaction and see if it matches the query. I have no idea how efficient this would be though, but I'm going to test and find out šŸ˜‰

p-himik12:02:27

A small correction - all level 2 subscriptions are recalculated on each app-db change.

ribelo11:02:55

I'm currently reading the penpot code from cover to cover, they have a slightly different approach to state management there.

ribelo11:02:38

exactly based on streaming

roman01la11:02:02

Penpot is UXbox right? I think posh's change inference wasn't always correct at the time, curious if that's changed

ribelo11:02:41

yes, uxbox is now a penpot

ribelo11:02:25

and yes, posh sometimes returned incorrect results, especially when retracting from db

ribelo11:02:19

by the way, datascript is not fast at any time, in fact performance is poor

ribelo11:02:38

with and without posh

roman01la11:02:15

That's my experience with datascript as well

ribelo12:02:22

simple flat db in the form of a {?id ?map} searched with meander is just as fast for query and many times faster when it comes to transactions or pull

lilactown17:02:39

if it's interesting to y'all, I was inspired by trying to use datascript + re-frame to author this lib: https://github.com/lilactown/autonormal

lilactown17:02:03

it normalizes your data into a relatively flat, simple map. and gives an API to easily pull data out using EQL; though of course you can just get-in to get specific data out too

lilactown17:02:58

it's not the power of datalog but I found that datalog wasn't what I wanted most of the time anyway when relying on datascript to store my app's entities; it wasn't performant enough

ribelo17:02:34

somewhere here I posted autonormal as an example šŸ˜‰

3
lilactown17:02:45

I see that now šŸ˜„ skimmed too fast

lilactown17:02:09

i'm hoping to eventually base a "pathom-client" lib on it but in the meantime, I find it moderately useful just for getting out of the "place-oriented" ness that y'all have been talking about

lilactown17:02:13

it still relies on parameterizing your subs/events to talk about specific entities tho. which is why i typically only use app-db for domain data and let UI state live in local component state

phronmophobic20:02:14

I recently wrote my thoughts on pretty much the same topic. Also, pretty much everyone here wrote something that influenced how I think about user interfaces! Thanks! (Tony Kay from fulcro and the minds behind hoplon were also big influences). > An entity can use three main techniques to refer to another entity: nesting, identifiers, and stateful references. > - Clojure Applied Chapter 1 The main difference between using datascript and subscriptions isn't flat vs nested, it's the type of reference that is used. Applications that use datascript as a data model tend to use identifiers as references whereas re-frame uses stateful references. What makes a component "reusable" isn't the type of reference that is used, it's whether or not the information needed to produce a reference is passed as an argument to the component or is hard coded in the component. When using identifiers, passing all the necessary information to build a reference is trivial (it's just the identifier). For data models that use nesting or stateful references, it's less straightforward. The tricky part with users interfaces is that there tends to be a lot of incidental state. Given the choice between implicit state handling that's less reusable and explicit state handling that's more reusable, developers tend to prefer implicit state handling. Ideally, incidental state should be handled implicitly and essential state should be handled explicitly which would make it easier to write reusable components by default. Here's the long version: https://blog.phronemophobic.com/reusable-ui-components.html

šŸ˜€ 3
ribelo21:02:15

Creating components is one thing

ribelo21:02:56

Using re-frame, always at some point I have problems with data denormalization and map fatigue in general. On the other hand, when e.g. I use datascript, or other ideas for flat normalized db, the problem is performance and synchronization with backend and/or local-storage. Some data should be excluded from storage, what is trivial in case of nested maps, but more complicated in case of flat structure.

ribelo21:02:41

every solution has its weaknesses

mikethompson22:02:28

@smith.adriane paths are identities in re-frame (paths within app-db)

šŸ‘ 3
phronmophobic22:02:23

so is it more correct to say re-frame uses both nesting and stateful references or just paths/nesting?

mikethompson22:02:25

I mention this only because I'm not sure what "stateful references" are. I know, I know, I should read your page.

phronmophobic22:02:46

I don't actually explain "stateful references"

mikethompson22:02:02

Then I'm off the hook :-)

phronmophobic22:02:22

I just kind of ignore them, but I give re-frame as an example of a library that uses stateful references

phronmophobic22:02:38

so I can update it if I'm wrong

mikethompson22:02:50

See the link provided above

mikethompson22:02:48

I have actually rewritten that page at some time, but never published the rewrite. I'm stuck on the final part of how best to get around placefulness

phronmophobic22:02:41

it's a tough problem.

mikethompson22:02:03

Tradeoffs all the way down.

phronmophobic22:02:45

I've been using macros to "track" usage of data derived from props to automatically produce references based on paths/nesting, but I'm not sure it's the right solution

mikethompson22:02:09

I can remember once spending an entire weekend trying to make Datascript faster, by turning it into a column store database and using the GPU. (GPUs are very fast with vectors) I failed. :-)

mikethompson22:02:34

And when I say I failed, I mean I failed at the very first hurdle: using the GPU.

phronmophobic22:02:40

the other approaches I've looked at: ā€¢ Om (uses proxies). After using it, it was ok, but had too many caveats for me to want to try again ā€¢ macros (my current approach) ā€¢ fulcro has ui components define queries as part of the component definition. I think that approach could also work, but I'd rather the queries were automatically written for me.

mikethompson22:02:15

Sorry, it was the section above the link I supplied

phronmophobic22:02:30

I like Clojure Applied's definition: > An entity can use three main techniques to refer to another entity: nesting, identifiers, and stateful references.

mikethompson22:02:28

I'm not reading that and feeling wiser :-)

phronmophobic22:02:44

path's within an app-db being the same as "nesting"

phronmophobic22:02:51

It's been tough finding good resources on the subject. Re-frame's docs are one of the best resources I could

phronmophobic22:02:44

It has been interesting comparing different approaches. Re-frame mainly focuses on path/nesting references and fulcro focuses on identifers as references

mikethompson22:02:46

An identity is something which identifies an entity. And it is very context specific.

šŸ‘ 3
mikethompson22:02:52

In C we used pointers

mikethompson22:02:12

In a database, it is a unique key

mikethompson22:02:32

In Clojure it is typically a keyword

mikethompson22:02:57

Or a path of keywords and integers (a path)

phronmophobic22:02:36

I think it's possible to write UI components in a way such that they're independent of which reference type is required, but it's a challenge to design it in a way that it's not an abstract mess.

phronmophobic22:02:57

The identity depends on the application's data model, not the language (although languages will usually have a lot to say about what kind of data model is idiomatic).

mikethompson22:02:39

Yeah, that's a distinction without a difference for me. Practically speaking. But maybe I'm missing something.

mikethompson22:02:15

I have to go but I present this challenge ... What is the identity of a certain collection of customers (the big ones)

mikethompson22:02:43

So just to be clear, I'm not talking about individual customers

mikethompson22:02:55

I'm talking about the collection of these customers

phronmophobic22:02:11

is it a subset or the full collection?

mikethompson22:02:16

Because I might need to distinguish good customers from bad ones

mikethompson22:02:41

Yeah, so how do I identify the two collections.

mikethompson22:02:56

Now I want to show the user this collection, now I want to show the other collection

p-himik22:02:49

It depends on the implementation. A single customer can be ID'ed by [:customer 7], for example. A collection of customers by [:customer [1 2 3]], for example. Of course, this precludes having a single customer having an ID that's a collection, but I think there can be reasonable limitations. Another way would be something like [[:customer 1] [:customer 2] [:customer 3]], similar limitations apply. And there are many other ways - again, depends on the implementation. And perhaps the particular set of limitations/idiosyncrasies you're willing to deal with.

šŸ‘ 3
āž• 3
phronmophobic22:02:03

;; uses nesting and identifiers
(def data
  {:customers {0 {:name "Bob"}
               1 {:name "Mary"}
               2 {:name "Sue"}}
   :lists {0 {:name "good"
              :customers [[:customers 0]
                          [:customers 2]]}
           1 {:name "bad"
              :customers [[:customers 1]]}}})

;; reference is [:lists 0] or [:lists 1]

;; alternative data model
(def data
  {"b4931d86-efca-4e86-b397-e54315673d32" {:name "Bob"}
   "133e9085-9481-4a64-befd-ce3b071a4c8a" {:name "Mary"}
   "f3c630f5-35e2-4600-8bf6-dc6fdbe76dae" {:name "Sue"}
   
   "b97c51b5-1078-45a9-a325-b8cb71f0959d" {:name "good"
                                           :customers ["b4931d86-efca-4e86-b397-e54315673d32"
                                                       "f3c630f5-35e2-4600-8bf6-dc6fdbe76dae"]}
   "51bf61bf-5163-4fd9-a52a-060de35ab7f7" {:name "bad"
                                           :customers ["133e9085-9481-4a64-befd-ce3b071a4c8a"]}})

;; references are "b97c51b5-1078-45a9-a325-b8cb71f0959d" and "51bf61bf-5163-4fd9-a52a-060de35ab7f7"

;; stateful references
;; not encouraged!
(let [bob (ref {:name "bob"})
      mary (ref {:name "Mary"})
      sue (ref {:name "Sue"})]
  (def customers [bob mary sue])
  (def good-customers (ref [bob sue]))
  (def bad-customers (ref [mary])))

;; references are good-customers and bad-customers

mikethompson23:02:22

@U2FRKM4TW yeah I was imagining that there might be two collections at different points within app-db. I was attempting to draw out that path is identity in re-frame, collections have identity. So too do the customer entities with them.

p-himik00:03:47

I see, right. Although "path is identity" and "[sub-]collections have identity" don't fit quite well in my head (by sub-collection I mean a subset of entities within some existing set). No matter what path-as-identity a sub-collection might have, it will require some special path handling because you won't be able to simply get-in.

mikethompson00:03:50

@smith.adriane Some some web apps are a thin veneer over a remote, authoritative database. In that case, database identities tend to dominate. In other cases, the SPA manages quite a complicated data model itself, without much reference to an authoritative remote database, in which case path based identities will be very useful.

šŸ‘ 3