Last active
August 16, 2016 21:06
-
-
Save whilo/a8ef2cd3f0e033d3973880a2001be32a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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)) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I have updated the gist to incorporate cljs-zones in benchmark 8 and 9.