Fork me on GitHub
#clojurescript
<
2022-02-19
>
mauricio.szabo01:02:09

Hi folks: I'm looking for suggestions, or tips, for a library. I want something like re-frame - I need to listen to events (onClick, onChange, onKeyUp, etc) and make then trigger other events/do actions. The trouble: these need to be configurable in user-space (imagine a user changing keybindings on an editor - that's exaclty one of the cases), it needs to be fast, and it needs to allow for async and sync dispatch (for example, if the user listen to an event onChange in an input, and decide to revert whatever was typed, I don't want the UI to flicker). The idea is to prototype a code editor. I'm even thinking about intercepting datascript transact! so I could have the app state in a quasi-real database, but I'm worried about subscribing to events. So, anyone have any suggestion? Even things like "yeah, re-frame is fast enough" can help! Thanks a lot! 🙂

muhanga12:02:36

Hi, all. Say, I am in position to add a clojuresript frontend to the existing clojure/java project. And I am also don't want to duplicate my "model" (entity maps, specs and so on) logic. Is it is acceptable approach to move model logic to the cljc files so it can be shared between frontend and backend? I am really interested in opinions here. I have seen a logic shared in cljc files, but I didn't seen (yet) a model logic shared in this way.

borkdude13:02:22

(defprotocol Foo)
(instance? Foo (reify Foo))
This returns true in Clojure but false in CLJS. What is the most performant alternative: implements?

lilactown17:02:48

implements? isn't documented but reading it I think that it would be faster than satisfies? - at least, it is nearly the same code just eliding the branch that checks native-satisfies?

lilactown17:02:50

(moved the rationale for why the instance? check does not work to #cljs-dev)

🙏 1
borkdude19:02:30

@lilactown would this be a good reason to introduce a deftype that implements a protocol rather than using reify ?

borkdude19:02:38

the implements? check isn't in a very hot spot though, so using implements? or even satisfies? is fine here. just wonder if there are other benefits to not using reify and rather moving to a deftype in CLJS. it still seems to be very fast with reify

borkdude19:02:53

hmm

cljs.user=> (defprotocol Foo)
false
cljs.user=> (deftype Bar [] Foo)
cljs.user/Bar
cljs.user=> (instance? Foo (->Bar))
false

borkdude19:02:57

Making a type seems to have a performance edge when invoking the protocol function on it though

lilactown19:02:24

@borkdude this is not a property of reify but protocols in general. they do not add themselves as parent objects in JS prototype chains, so instance? will always return false if you're checking if something implements a protocol

lilactown19:02:36

you ought to use implements? for this

borkdude19:02:50

makes sense, thanks.

lilactown19:02:01

it seems weird to me that it works on the JVM tbh. I assume it's a property of how protocols are reified and interact with the Java object model

borkdude19:02:04

if I make a type I can just do the instance check on the type though, this is what I did before

lilactown19:02:48

that's true. if you know something will always be of a concrete type, it's much faster to check

borkdude19:02:11

I can arrange it as such

borkdude19:02:36

would there be any reason why invoking a protocol fn on a reify would be slower than on a typed instance (deftype) in CLJS?

lilactown19:02:27

I'd have to see the code. I would think that dispatching would be the same, but constructing the object may be slower for reify

lilactown19:02:29

i.e. the difference may be in (reify ,,,) vs (->Type ,,,)

borkdude19:02:33

I did a huge refactor to go from a type that wraps a function to a protocol because it was slightly faster in Clojure, but now I'm discovering it's way slower in CLJS :/ I hope I can speed it up using some tricks still.

borkdude19:02:48

I'm not referring to the construction, only to the invocation

lilactown19:02:42

> from a type that wraps a function to a protocol not sure what this means. I thought you were asking about protocols via reify vs protocols via deftype

borkdude19:02:30

yes. so first I had this:

(deftype Foo [f])

(if (instance? Foo ...) ((.-f obj) ... ...) ...)

borkdude19:02:03

and now I went to:

(defprotocol IFoo (invoke [_]))
(invoke (reify IFoo (invoke [_])))

borkdude19:02:15

the second one is way slower in CLJS unfortunately

borkdude19:02:20

but faster in Clojure.

borkdude19:02:41

I mean, pull out the construction in a let

borkdude19:02:45

and then invoke many times

lilactown19:02:05

ok, and what about

(defprotocol IFoo (invoke [_]))

(deftype Foo [f] IFoo (invoke [_] (f)))

(let [foo (->Foo)]
  (invoke foo)) 
?

lilactown19:02:57

I thought that's what we were comparing reify to

borkdude19:02:15

that's what I'm considering next

borkdude19:02:19

and was asking about

lilactown19:02:49

method/property invocation is quite fast in JS. hard to get faster than ((.-f obj) ,,,)

borkdude19:02:05

Exactly. Then why is using protocols faster in Clojure :(

borkdude19:02:50

I mean, it's a cost to maintain two different styles

lilactown20:02:47

you can see what the JS code is doing for this code

(defprotocol Foo
  (bar [o]))
becomes
cljs.user.Foo = function(){};

cljs.user.bar = (function cljs$user$bar(o){
  if((((!((o == null)))) && ((!((o.cljs$user$Foo$bar$arity$1 == null)))))){
    return o.cljs$user$Foo$bar$arity$1(o);
  } else {
    var x__18528__auto__ = (((o == null))?null:o);
    var m__18529__auto__ = (cljs.user.bar[goog.typeOf(x__18528__auto__)]);
    if((!((m__18529__auto__ == null)))){
      return m__18529__auto__.call(null,o);
    } else {
      var m__18526__auto__ = (cljs.user.bar["_"]);
      if((!((m__18526__auto__ == null)))){
        return m__18526__auto__.call(null,o);
      } else {
        throw cljs.core.missing_protocol.call(null,"Foo.bar",o);
      }
    }
  }
});

lilactown20:02:26

how clojure compiles protocol invocations isn't clear to me, I just know "clojure protocols are fast"

lilactown20:02:20

have you profiled your code?

lilactown20:02:45

you might also test this using optimizations, the difference may be smaller

borkdude20:02:05

I have profiled yes, using advanced. Btw, I now see that reify or type doesn't really make a difference (from this simple benchmark).

cljs.user=> (defprotocol IFoo (invoke [_]))
cljs.user=> (deftype Dude [] IFoo (invoke [_] (inc 1)))
cljs.user=> (let [f (->Dude)] (simple-benchmark [f f] (invoke f) 1000000000))
[f f], (invoke f), 1000000000 runs, 21015 msecs
nil
cljs.user=> (let [f (reify IFoo (invoke [_] (inc 1)))] (simple-benchmark [f f] (invoke f) 1000000000))
[f f], (invoke f), 1000000000 runs, 21122 msecs
nil
cljs.user=> (deftype Foo [f])
cljs.user/Foo
cljs.user=> (let [f (->Foo inc)] (simple-benchmark [f f] ((.-f f) 1) 1000000000))
[f f], ((.-f f) 1), 1000000000 runs, 6447 msecs
nil

lilactown20:02:22

((.-f f) 1) may just be more JIT friendly

lilactown20:02:43

but it also elides all the checks you see in the output above

borkdude20:02:43

damnit, a whole day of refactoring... bummer :)

lilactown20:02:28

I wonder if there would be room for an "unguarded" protocol dispatch in cljs.core

borkdude20:02:10

maybe I can patch it for my own protocol?

lilactown20:02:07

in the case you have above you ought to be a able to compile it to

o.cljs$user$IFoo$invoke$arity$1()

borkdude20:02:32

maybe I can invoke that sucker directly?

lilactown20:02:46

you might be able to do it via a macro

1
borkdude20:02:57

cljs.user=> (let [f (reify IFoo (invoke [_] (inc 1)))] ((.-cljs$user$IFoo$invoke$arity$1 f)))
2

borkdude20:02:23

cljs.user=> (let [f (reify IFoo (invoke [_] (inc 1)))] (simple-benchmark [f f] ((.-cljs$user$IFoo$invoke$arity$1 f)) 1000000000))
[f f], ((.-cljs$user$IFoo$invoke$arity$1 f)), 1000000000 runs, 496 msecs
nil

borkdude20:02:48

now we're talking

borkdude20:02:35

Using that "patch", it's close enough to the (.-f ..) approach:

cljs.user=> (let [f (reify IFoo (invoke [_] (inc 1)))] (simple-benchmark [f f] (invoke f) 1000000000))
[f f], (invoke f), 1000000000 runs, 7400 msecs
nil

borkdude20:02:54

Eh, the patch being:

cljs.user=> (set! cljs.user.invoke (fn [this] ((.-cljs$user$IFoo$invoke$arity$1 this) this)))
#object[Function]

lilactown20:02:17

does that survive advanced optimizations?

borkdude20:02:14

I wonder about that too

borkdude20:02:26

but that's easy enough to find out

lilactown20:02:01

I think it would

lilactown20:02:06

that's great!

borkdude20:02:27

I don't think this is going to work out though

borkdude20:02:03

the check is there for objects that do not have this method attached to them, e.g. if you would extend the protocol to :default and pass a number or string which does not implement the protocol

lilactown20:02:27

yeah that's right. you are giving up some amount of polymorphism for speed

lilactown20:02:49

I thought for your case you could rely on a concrete type being passed in?

borkdude20:02:37

That's right, I can do something like this:

#?(:cljs
   (set! sci.impl.types/eval
         (fn [this ctx bindings]
           (if-let [f (.-sci$impl$types$Eval$eval$arity$3 this)]
             (f this ctx bindings)
             this))))

borkdude20:02:34

This is very similar to what I had before

borkdude20:02:42

with the .-f thing

borkdude20:02:09

That was like:

(if (instance? Foo ...) ((.-f ...) ...) ...)

lilactown20:02:57

that looks pretty similar to what the body of sci.impl.types.eval ought to be. but again, this might be far more JIT friendly

borkdude20:02:42

> Cannot read property 'call' of undefined:thinking_face:

borkdude20:02:49

Aaah!

#?(:cljs
   (set! sci.impl.types/eval
         (fn [expr ctx bindings]
           (if-some [_ (.-sci$impl$types$Eval$eval$arity$3 expr)]
             (.sci$impl$types$Eval$eval$arity$3 expr ctx bindings)
             expr))))

borkdude20:02:56

that works :)

borkdude20:02:25

This is what it translates into:

"function (expr,ctx,bindings){\nvar temp__5792__auto__ = expr.sci$impl$types$Eval$eval$arity$3;\nif((temp__5792__auto__ == null)){\nreturn expr;\n} else {\nvar _ = temp__5792__auto__;\nreturn expr.sci$impl$types$Eval$eval$arity$3(ctx,bindings);\n}\n}"

lilactown20:02:44

o_O strange that calling f didn't work

borkdude20:02:14

This now works:

cljs.user=> (sci/eval-string "[1 (+ 1 2 3)]")
[1 6]
but not everything works...

borkdude20:02:02

Cannot set property '0' of undefined

borkdude20:02:51

anyway, stuff to figure out. I hope this will fix the perf issues, else I'll probably roll back to what it was

borkdude20:02:56

another fix:

#?(:cljs
   (set! sci.impl.types/eval
         (fn [expr ctx bindings]
           (when-some [expr expr]
             (if-some [_ (.-sci$impl$types$Eval$eval$arity$3 expr)]
               (.sci$impl$types$Eval$eval$arity$3 expr ctx bindings)
               expr)))))

borkdude21:02:18

This seems to work in all cases:

(def old-eval eval)

#?(:cljs
   (set! sci.impl.types/eval
         (fn [expr ctx bindings]
           (if (satisfies? Eval expr)
             (old-eval expr ctx bindings)
             expr))))
If I implement everyone on a type I could replace satisfies? with instance? and then call old-val...

borkdude21:02:30

but old-eval was slow to call in the first place

borkdude21:02:11

if I do this, then it doesn't work anymore:

(def old-eval eval)

#?(:cljs
   (set! sci.impl.types/eval
         (fn [expr ctx bindings]
           (if (satisfies? Eval expr)
             (.sci$impl$types$Eval$eval$arity$3 expr ctx bindings)
             expr))))

borkdude21:02:58

but I could replace that property access with the concrete type property perhaps

borkdude21:02:07

so then I'm mostly back to where I was, that would be great

borkdude21:02:41

something to try tomorrow, thanks for the thinking along!

borkdude22:02:31

This is also interesting. This is what I get on master:

cljs.user=> (time (sci/eval-string "(loop [val 0 cnt 10000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val))"))
"Elapsed time: 2560.479166 msecs"
10000000
But on my protocol branch, the first time:
cljs.user=> (time (sci/eval-string "(loop [val 0 cnt 10000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val))"))
"Elapsed time: 1666.945417 msecs"
10000000

cljs.user=> (time (sci/eval-string "(loop [val 0 cnt 10000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val))"))
"Elapsed time: 3654.658507 msecs"

borkdude22:02:44

and after that, you see it becomes 2x slower and it stays that way after that

borkdude22:02:01

the first time it was even faster than on master... :thinking_face:

borkdude22:02:02

Fascinating. On master:

$ node $(which nbb)
Welcome to nbb v0.1.8!
user=> (time (loop [val 0 cnt 10000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val)))
"Elapsed time: 865.337636 msecs"

borkdude22:02:18

With protocols:

$ node out/nbb_main.js
Welcome to nbb v0.1.8!
user=>  (time (loop [val 0 cnt 10000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val)))
"Elapsed time: 571.292925 msecs"
10000000
user=>  (time (loop [val 0 cnt 10000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val)))
"Elapsed time: 1045.600417 msecs"
10000000

borkdude22:02:22

you see that first time??

borkdude22:02:51

It's not that much slower in advanced as you can see, so it might be ok to keep it... although it's not an improvement in general... I might take this small hit on CLJS because in the JVM it's faster... it's a balance

borkdude22:02:13

After running more runs of these it could be fair to say that it's not that much of a difference in advanced

borkdude22:02:45

Both around 1s

borkdude22:02:27

This is pretty weird. The first time it seems to be significantly faster.

user=> (time (loop [val 0 cnt 20000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val)))
"Elapsed time: 1097.261099 msecs"
20000000
user=> (time (loop [val 0 cnt 20000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val)))
"Elapsed time: 1994.345612 msecs"
20000000
user=> (time (loop [val 0 cnt 20000000] (if (pos? cnt) (recur (inc val) (dec cnt)) val)))
"Elapsed time: 1983.203614 msecs"
20000000

borkdude22:02:39

but after that it becomes 2x slow

borkdude10:02:01

Programmed my way out using a macro which does 1 thing on Clojure and another thing on CLJS...!

babashka 2
lilactown00:02:33

@borkdude i wonder if you were hitting the same thing that this timely tweet thread is about https://twitter.com/SeaRyanC/status/1496273931120300032?s=20