Last time I needed to speed up some code, I wrote a Clojure macro that recorded the aggregate time spent executing the code wrapped by the macro. Aggregate timings were useful since the same functions were called multiple times in the code path we were trying to optimize. Seeing total times made it easier to identify where we should spend our time.
Below is the namespace I temporarily introduced into our codebase.
(ns metrics)(defn msec-str"Returns a human readable version of milliseconds based upon scale"[msecs](let [s1000m(* 60s)h(* 60m)](condp>= msecs1(format"%.5f msecs"(float msecs))s(format"%.1f msecs"(float msecs))m(format"%.1f seconds"(float (/ msecss)))h(format"%02dm:%02ds"(int (/ msecsm))(mod(int (/ msecss))60))(format"%dh:%02dm"(int (/ msecsh))(mod(int (/ msecsm))60)))))(def aggregates(atom{}))(defmacro record-aggregate"Records the total time spent executing body across invocations."[label&body]`(do(when-not (contains? @aggregates~label)(swap!aggregatesassoc ~label{:order(inc (count @aggregates))}))(let [start-time#(System/nanoTime)result#(do ~@body)result#(if (and (seq? result#)(instance? clojure.lang.IPendingresult#)(not (realized?result#)))(doall result#)result#)end-time#(System/nanoTime)](swap!aggregatesupdate-in[~label:msecs](fnil+ 0)(/ (double (- end-time#start-time#))1000000.0))result#)))(defn log-times"Logs time recorded by record-aggregate and resets the aggregate times."[](doseq [[labeldata](sort-by (comp :ordersecond)@aggregates):let[msecs(:msecsdata)]](println "Executing"label"took:"(msec-strmsecs)))(reset!aggregates{}))
record-aggregate takes a label and code and times how long that code takes to run. If the executed code returns an unrealized lazy sequence, it also evaluates the sequence1.
Below is an example of using the above code. When we used it, we looked at the code path we needed to optimize and wrapped chunks of it in record-aggregate. At the end of the calculations, we inserted a call to log-times so timing data would show up in our logs.
Using this technique, we were able to identify slow parts of our process and were able to optimize those chunks of our code. There are potential flaws with measuring time like this, but they were not a problem in our situation2.
Looking forward to the next article? Never miss a post by subscribing using e-mail or RSS. The e-mail newsletter goes out periodically (at most once a month) and includes reviews of books I've been reading and links to stuff I've found interesting.