Learn a practical JVM measurement workflow for Clojure concurrency: isolate pure code, run realistic load, inspect queues and threads, and avoid misleading microbenchmarks.
Benchmarking concurrent Clojure code has the same trap as benchmarking Java concurrency: measuring a tiny expression rarely tells you how the system behaves under real load. You need both microbenchmarks for focused functions and profiling for the full runtime shape.
Use the same discipline you would use on the JVM in Java: warm up, repeat, isolate variables, compare like with like, and verify the result in a profiler.
| Question | Measurement tool |
|---|---|
| Is this pure function allocating too much? | Criterium or another focused benchmark. |
| Is shared state contended? | Load test plus retry/queue metrics. |
| Are threads blocked? | JFR, VisualVM, YourKit, or thread dumps. |
| Is GC affecting latency? | JVM GC logs, JFR allocation events, or profiler allocation views. |
| Is Java interop expensive? | Profile the hot path and inspect reflection/boxing/conversion. |
Do not ask one benchmark to answer all of these questions.
Pure functions are easiest to benchmark because they do not retain state between runs.
1(require '[criterium.core :as crit])
2
3(defn summarize-events
4 [events]
5 (reduce (fn [summary {:keys [type]}]
6 (update summary type (fnil inc 0)))
7 {}
8 events))
9
10(def sample-events
11 (vec (for [n (range 10000)]
12 {:id n
13 :type (if (even? n) :order/created :order/updated)})))
14
15(crit/quick-bench
16 (summarize-events sample-events))
This answers a narrow question: how expensive is the local transformation? It does not answer whether atom contention, database writes, or executor saturation will hurt production throughput.
When testing coordination, include the coordination.
1(defn run-counter-load!
2 [worker-count updates-per-worker]
3 (let [counter (atom 0)
4 workers (doall
5 (for [_ (range worker-count)]
6 (future
7 (dotimes [_ updates-per-worker]
8 (swap! counter inc)))))]
9 (doseq [worker workers]
10 @worker)
11 @counter))
12
13(run-counter-load! 8 10000)
14;; => 80000
This is a coarse load probe, not a perfect benchmark. It is useful because it exercises futures, scheduling, atom contention, and joins together. If this number looks suspicious, move to a proper load harness and profiler.
Most useful Clojure performance evidence still appears as JVM evidence.
| Profiler signal | Clojure interpretation |
|---|---|
| Many threads parked or blocked | The bottleneck may be I/O, executor capacity, or queue backpressure. |
Hot clojure.lang collection operations |
Review allocation, lazy realization, and intermediate collections. |
| High time in atom or ref update paths | Check contention, transaction size, and update frequency. |
| Reflection warnings or reflective calls | Add type hints or move interop out of the inner loop. |
| High allocation in sequence processing | Consider eager realization, reducers, transients, or chunking after profiling. |
Run with realistic data shape. A benchmark over ten tiny maps can miss costs that appear only with real payloads, nested values, or Java object conversion.
When comparing a Clojure implementation to Java code, hold the engineering variables steady.
| Variable | Keep consistent |
|---|---|
| JDK | Same version, same JVM flags, same hardware. |
| Workload | Same data size, blocking behavior, and success/error mix. |
| Warm-up | Enough iterations for JIT compilation and stable allocation behavior. |
| State model | Compare equivalent consistency guarantees, not atom updates against unsafe mutation. |
| Output | Verify both versions produce the same result under concurrency. |
If the Java version mutates unsafely and the Clojure version preserves correctness, the speed comparison is not meaningful.
| Check | Failure mode avoided |
|---|---|
| Separate pure-function benchmarks from load tests. | Confusing local CPU cost with system throughput. |
| Recreate state for each benchmark sample when needed. | State leaking between runs. |
| Avoid printing or logging in measured loops. | Measuring console I/O instead of program logic. |
| Use a profiler after a surprising benchmark. | Optimizing the wrong layer. |
| Record the JDK, flags, data shape, and hardware. | Results that cannot be reproduced. |