Skip to content

Instantly share code, notes, and snippets.

@ptaoussanis
Forked from danownsthisspace/stratch.clj
Last active December 9, 2021 18:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ptaoussanis/9931fbc7dc93a8ecc54dd3daaead6c41 to your computer and use it in GitHub Desktop.
Save ptaoussanis/9931fbc7dc93a8ecc54dd3daaead6c41 to your computer and use it in GitHub Desktop.
A function wrapper that ensures that a function can only be called once
(ns scratch.core
(:require [taoensso.encore :as enc]))
(defn wrap-once-in-a-while-1
"Uses atom + `swap-vals!`"
[^long msecs-period f]
(let [last-executed_ (atom 0)]
(fn wrapper [& args]
(let [[old new]
(swap-vals! last-executed_
(fn [^long last-executed]
(let [now (System/currentTimeMillis)
elapsed (- now last-executed)]
(if (>= elapsed msecs-period)
now
last-executed))))
execute? (not= old new)]
(when execute?
(try
(do {:okay (apply f args)})
(catch Throwable t {:error t})))))))
(defn wrap-once-in-a-while-2
"Uses atom + lock"
[^long msecs-period f]
(let [last-executed_ (atom 0)
lock (Object.)]
(fn wrapper [& args]
(let [execute?
(locking lock
(let [now (System/currentTimeMillis)
elapsed (- now ^long @last-executed_)]
(when (>= elapsed msecs-period)
(reset! last-executed_ now))))]
(when execute?
(try
(do {:okay (apply f args)})
(catch Throwable t {:error t})))))))
(defn wrap-once-in-a-while-3
"Uses atom + manual `compare-and-set!` loop"
[msecs-period f]
(let [last-executed_ (atom 0)]
(fn wrapper [& args]
(let [execute?
(loop []
(let [now (System/currentTimeMillis)
^long last-executed @last-executed_
elapsed (- now last-executed)]
(when (>= elapsed ^long msecs-period)
(if (compare-and-set! last-executed_ last-executed now)
true
(recur)))))]
(when execute?
(try
(do {:okay (apply f args)})
(catch Throwable t {:error t})))))))
(comment
(do
(def f0 (fn [])) ; Control, without wrapper
(def f1 (wrap-once-in-a-while-1 10 (fn [])))
(def f2 (wrap-once-in-a-while-2 10 (fn [])))
(def f3 (wrap-once-in-a-while-3 10 (fn []))))
;; Quick+dirty single-thread comparative bench
(enc/quick-bench 1e6 (f0) (f1) (f2) (f3)) ; [35.98 95.64 66.31 64.26]
;; Quick+dirty multi-thread comparative bench
(enc/quick-bench 1e4
(let [futures (repeatedly 10 (fn [] (future (f0))))] (run! deref futures))
(let [futures (repeatedly 10 (fn [] (future (f1))))] (run! deref futures))
(let [futures (repeatedly 10 (fn [] (future (f2))))] (run! deref futures))
(let [futures (repeatedly 10 (fn [] (future (f3))))] (run! deref futures))) [507.57 523.9 521.53 517.19]
)
@ptaoussanis
Copy link
Author

A very simple set of comparative benchmarks, just to demo what a comparison might look like.

The key results (times) are [35.98 95.64 66.31 64.26], [507.57 523.9 521.53 517.19]

Some quick conclusions, at least for these numbers:

  • All implementations add pretty trivial absolute amount of cost to single-threaded calls.
  • swap-vals! implementation is the slowest - at about 140% cost of the lock / CAS implementations.
  • lock and CAS implementations costs are similar.
  • The multi-threaded times aren't too interesting since all times seem to be dominated by factors outside the wrappers.

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