Skip to content

Instantly share code, notes, and snippets.

@whilo
Last active August 16, 2016 21:06
Show Gist options
  • Save whilo/a8ef2cd3f0e033d3973880a2001be32a to your computer and use it in GitHub Desktop.
Save whilo/a8ef2cd3f0e033d3973880a2001be32a to your computer and use it in GitHub Desktop.
(ns full.binding-test
#?(:cljs (:require [cljs.core.async :refer [<! chan onto-chan]]
[zones.core :as zones :include-macros true]))
#?(:cljs (:require-macros
[full.async :refer [<<!]]
[full.binding-test :refer
[new-bound-fn create-vars new-binding]]
[cljs.core.async.macros :refer [go go-loop]])))
#?(:cljs (enable-console-print!))
;; Binding approach:
;; We only track the active bindings which are currently rebound
;; through the binding macro (could do it for with-redefs as well). We
;; do reference counting here to do minimal work. The cost is only paid
;; by the user of binding and the async mechanisms supporting binding.
;; Explicit opt-out of the tracking mechanism using the old binding
;; would also be possible.
#?(:cljs
(def active-bindings #js {}))
#?(:cljs
(defn resolve-frame [active-bindings]
(let [res #js []
ks (js/Object.keys active-bindings)]
(loop [i 0]
(when (< i (alength ks))
(let [b (aget active-bindings (aget ks i))]
(.push res #js [(aget b "setter")
((aget b "getter"))]))
(recur (inc i))))
res)))
(defn restore [res-frame]
(loop [i 0]
(when (< i (alength res-frame))
((aget (aget res-frame i) 0)
(aget (aget res-frame i) 1))
(recur (inc i)))))
#?(:cljs
(defn inc-binding [sym setter getter]
(if-let [b (aget active-bindings sym)]
(aset b "count" (inc (aget b "count")))
(aset active-bindings sym
#js {"count" 0
"setter" setter
"getter" getter}))))
(comment
(inc-binding 'foo inc dec)
(macroexpand-1 '(inc-binding foo 42)))
#?(:cljs
(defn dec-binding [sym]
(let [b (aget active-bindings sym)
c (aget b "count")]
(if (pos? c)
(aset b "count" (dec c))
(js-delete active-bindings sym)))))
(comment
(dec-binding 'foo))
#?(:clj
(defmacro new-binding [bindings & body]
(let [names (take-nth 2 bindings)
vals (take-nth 2 (drop 1 bindings))
tempnames (map (comp gensym name) names)
binds (map vector names vals)
resets (reverse (map vector names tempnames))
bind-value (fn [[k v]]
;; TODO fix fully qualified namespaced name
(let [sym# (str (:name (:ns &env)) "/" (pr-str k))]
(list 'do
`(inc-binding ~sym# (fn [v#] (set! ~k v#)) (fn [] ~k))
(list 'set! k v)
)))
unbind-value (fn [[k v]]
;; TODO fix fully qualified namespaced name
(let [sym# (str (:name (:ns &env)) "/" (pr-str k))]
(list 'do
`(dec-binding ~sym#)
(list 'set! k v)
)))]
`(let [~@(interleave tempnames names)]
~@(map bind-value binds)
(try
~@body
(finally
~@(map unbind-value resets)))))))
(comment
(def ^:dynamic foo 1)
(do
(println active-bindings)
(new-binding [foo 42]
(println active-bindings)
foo)
(println active-bindings)))
;; helper
#?(:clj
(defmacro create-vars [n]
`(do
~@(map (fn [n#] `(def ~(vary-meta (symbol (str "v" n#)) assoc :dynamic true) ~n#))
(range n)))))
#?(:cljs
(defn ^:export benchmark1 []
(create-vars 21)
(def res-frame (resolve-frame active-bindings))
(simple-benchmark [] (restore res-frame) 10000)))
#?(:cljs
(defn ^:export benchmark2 []
;; do a quick, not particularly smart analysis of impact of this binding for core.async
;; when no bindings are active.
(go (simple-benchmark [] (<! (go 42)) 10000)) ;; ~200 ms (Chromium)
))
#?(:cljs
(defn ^:export benchmark3 []
;; there is a little overhead (~10 %) caused by tracking active-bindings
(go (simple-benchmark []
(let [frame (resolve-frame active-bindings)]
;; ...
;; async scope
(let [old (resolve-frame active-bindings)]
(restore frame)
(<! (go 42))
(restore old))) 10000)) ;; ~220 ms (Chromium)
))
#?(:cljs
(defn ^:export benchmark4 []
;; an empty binding itself causes some overhead (the same for current binding macro)
(go (simple-benchmark []
(new-binding []
(let [frame (resolve-frame active-bindings)]
;; ...
;; async scope
(let [old (resolve-frame active-bindings)]
(restore frame)
(<! (go 42))
(restore old)))) 10000)) ;; ~300 ms (Chromium)
))
#?(:cljs
(defn ^:export benchmark5 []
;; now the user of binding pays roughly linear cost for every
;; binding that is active
(go (simple-benchmark []
(new-binding [v1 1 v2 2 v3 3 v4 4 v5 5]
(let [frame (resolve-frame active-bindings)]
;; ...
;; async scope
(let [old (resolve-frame active-bindings)]
(restore frame)
(<! (go 42))
(restore old)))) 10000)) ;; ~800 ms (Chromium)
))
#?(:cljs
(defn ^:export benchmark6 []
(go (simple-benchmark []
(new-binding [v1 1 v2 2 v3 3 v4 4 v5 5 v6 6 v7 7 v8 8 v9 9 v10 10]
(let [frame (resolve-frame active-bindings)]
;; ...
;; async scope
(let [old (resolve-frame active-bindings)]
(restore frame)
(<! (go 42))
#_((fn [a] (+ v1 v2 a)) 4)
(restore old)))) 10000)
#_(simple-benchmark []
(zones/binding [v1 1 v2 2 v3 3 v4 4 v5 5 v6 6 v7 7 v8 8 v9 9 v10 10]
((zones/bound-fn []
((fn [a] (+ v1 v2 a)) 4)))) 10000)) ;; ~1500 ms (Chromium)
))
#?(:cljs
(defn ^:export benchmark7 []
(go (simple-benchmark []
(new-binding [v1 1 v2 2 v3 3 v4 4 v5 5 v6 6 v7 7 v8 8 v9 9 v10 10
v11 11 v12 12 v13 13 v14 14 v15 15 v16 16 v17 17 v18 18 v19 19 v20 20]
(let [frame (resolve-frame active-bindings)]
;; ...
;; async scope
(let [old (resolve-frame active-bindings)]
(restore frame)
(<! (go 42))
(restore old)))) 10000)) ;; ~4200 ms (Chromium)
))
;; cost to establish bindings
#?(:cljs
(defn ^:export benchmark8 []
(simple-benchmark []
(binding [v1 1]
;; for a binding form we at least have to call one
;; function for it to make sense
((fn [foo] foo) v1)) 10000) ;; ~2 ms (Chromium)
(simple-benchmark []
(new-binding [v1 1]
((fn [foo] foo) v1)) 10000) ;; ~60 ms (Chromium)
(simple-benchmark []
(zones/binding [v1 1]
((fn [foo] foo) (zones/get v1))) 10000) ;; ~60 ms (Chromium)
))
#?(:clj
(defmacro new-bound-fn
"Now we can implement bound-fn accordingly."
[args & body]
`(let [frame# (resolve-frame active-bindings)]
(fn ~args
(let [old# (resolve-frame active-bindings)]
(restore frame#)
~@body
(restore old#))))))
;; cost to manage bindings
#?(:cljs (defn ^:export benchmark9 []
(simple-benchmark []
(new-binding [v1 1]
((new-bound-fn []
((fn [foo] (+ v1 foo)) v1)))) 10000) ;; ~60 ms (Chromium)
(simple-benchmark []
(zones/binding [v1 1]
((zones/bound-fn []
((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))) 10000)
(simple-benchmark []
(new-binding [v1 1]
((new-bound-fn []
((new-bound-fn []
((fn [foo] (+ v1 foo)) v1)))))) 10000) ;; ~60 ms (Chromium)
(simple-benchmark []
(zones/binding [v1 1]
((zones/bound-fn []
((zones/bound-fn []
((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))))) 10000)
(simple-benchmark []
(new-binding [v1 1]
((new-bound-fn []
((new-bound-fn []
((new-bound-fn []
((fn [foo] (+ v1 foo)) v1)))))))) 10000) ;; ~60 ms (Chromium)
(simple-benchmark []
(zones/binding [v1 1]
((zones/bound-fn []
((zones/bound-fn []
((zones/bound-fn []
((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))))))) 10000)
))
#?(:cljs (defn ^:export benchmark10 []
(simple-benchmark [] ((fn [] v1)) 10000)
(simple-benchmark [] ((fn [] (zones/get v1))) 10000)))
;; some examples runs
;; full.binding_test.benchmark9()
;; core.cljs:150 [], (new-binding [v1 1] ((new-bound-fn [] ((fn [foo] (+ v1 foo)) v1)))), 10000 runs, 44 msecs
;; core.cljs:150 [], (zones/binding [v1 1] ((zones/bound-fn [] ((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))), 10000 runs, 37 msecs
;; core.cljs:150 [], (new-binding [v1 1] ((new-bound-fn [] ((new-bound-fn [] ((fn [foo] (+ v1 foo)) v1)))))), 10000 runs, 58 msecs
;; core.cljs:150 [], (zones/binding [v1 1] ((zones/bound-fn [] ((zones/bound-fn [] ((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))))), 10000 runs, 94 msecs
;; core.cljs:150 [], (new-binding [v1 1] ((new-bound-fn [] ((new-bound-fn [] ((new-bound-fn [] ((fn [foo] (+ v1 foo)) v1)))))))), 10000 runs, 90 msecs
;; core.cljs:150 [], (zones/binding [v1 1] ((zones/bound-fn [] ((zones/bound-fn [] ((zones/bound-fn [] ((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))))))), 10000 runs, 88 msecs
;; full.binding_test.benchmark9()
;; core.cljs:150 [], (new-binding [v1 1] ((new-bound-fn [] ((fn [foo] (+ v1 foo)) v1)))), 10000 runs, 61 msecs
;; core.cljs:150 [], (zones/binding [v1 1] ((zones/bound-fn [] ((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))), 10000 runs, 29 msecs
;; core.cljs:150 [], (new-binding [v1 1] ((new-bound-fn [] ((new-bound-fn [] ((fn [foo] (+ v1 foo)) v1)))))), 10000 runs, 64 msecs
;; core.cljs:150 [], (zones/binding [v1 1] ((zones/bound-fn [] ((zones/bound-fn [] ((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))))), 10000 runs, 49 msecs
;; core.cljs:150 [], (new-binding [v1 1] ((new-bound-fn [] ((new-bound-fn [] ((new-bound-fn [] ((fn [foo] (+ v1 foo)) v1)))))))), 10000 runs, 92 msecs
;; core.cljs:150 [], (zones/binding [v1 1] ((zones/bound-fn [] ((zones/bound-fn [] ((zones/bound-fn [] ((fn [foo] (+ (zones/get v1) foo)) (zones/get v1))))))))), 10000 runs, 84 msecs
;; full.binding_test.benchmark10()
;; core.cljs:150 [], ((fn [] v1)), 10000 runs, 1 msecs
;; core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 4 msecs
;; null
;; full.binding_test.benchmark10()
;; core.cljs:150 [], ((fn [] v1)), 10000 runs, 2 msecs
;; core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 6 msecs
;; null
;; full.binding_test.benchmark10()
;; core.cljs:150 [], ((fn [] v1)), 10000 runs, 3 msecs
;; core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 4 msecs
#?(:cljs
(defn ^:export main []
(do
(def ^:dynamic v1 1)
(println "init" v1)
(new-binding [v1 5]
(println "bound" v1)
(js/setTimeout (new-bound-fn []
(println "init async" v1)
(binding [v1 42]
(println "rebound async" v1))
(println "end async" v1))
1000))
(println "end" v1))))
;; full.binding_test.benchmark10()
;; core.cljs:150 [], ((fn [] v1)), 10000 runs, 1 msecs
;; core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 4 msecs
;; null
;; full.binding_test.benchmark10()
;; core.cljs:150 [], ((fn [] v1)), 10000 runs, 2 msecs
;; core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 6 msecs
;; null
;; full.binding_test.benchmark10()
;; core.cljs:150 [], ((fn [] v1)), 10000 runs, 3 msecs
;; core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 4 msecs
(comment
(benchmark1))
@whilo
Copy link
Author

whilo commented Aug 16, 2016

I have updated the gist to incorporate cljs-zones in benchmark 8 and 9.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment