Liking cljdoc? Tell your friends :D

Benchmarks

Benchmarks were acquired using JMH and jmh-clojure.

Each test is for a single call to:

(org.openjdk.jmh.infra.Blackhole/consumeCPU 100)

This consumes 100 "time tokens" and care is taken to keep it from getting optimized out by the JIT. Each benchmark contains a "Baseline" measurement which is just the call to (Blackhole/consumeCPU 100) with no other calls.

Each test includes a "serial" run with one thread and a "parallel" run with 8 threads. The intention is to test the overhead Fusebox brings to applications, and thus each test is designed for threads to not have to block on Fusebox machinery (i.e. the optimal throughput case). For example, bulkhead is set to allow 25 threads so there is never contention to enter the bulkhead. This seems counterintuitive, but otherwise you end up measuring the latency of the operation under test (in this case Blackhole/consumeCPU).

Big shoutout to @jgpc42. If it weren't for his work on jmh-clojure, testing with JMH would have taken weeks instead of hours.

Charts were generated using clj-xchart.

Table of Contents

Settings

All tests were performed on a 14-inch, 2021 MacBook Pro:

  • Apple M1 Max
    • 8 performance cores
    • 2 efficiency cores
  • 32 GB
  • Sonoma 14.5
Normal
# JMH version: 1.32
# VM version: JDK 21.0.4, OpenJDK 64-Bit Server VM, 21.0.4
# VM invoker: /opt/homebrew/Cellar/openjdk@21/21.0.4/libexec/openjdk.jdk/Contents/Home/bin/java
# VM options: -XX:-OmitStackTraceInFastThrow -Dfusebox.usePlatformThreads=false -Dlogback.configurationFile=logback-dev.xml -Dclojure.basis=.cpcache/3045479295.basis
# Blackhole mode: full + dont-inline hint
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Sampling time
Direct Linking
# JMH version: 1.32
# VM version: JDK 21.0.4, OpenJDK 64-Bit Server VM, 21.0.4
# VM invoker: /opt/homebrew/Cellar/openjdk@21/21.0.4/libexec/openjdk.jdk/Contents/Home/bin/java
# VM options: -XX:-OmitStackTraceInFastThrow -Dfusebox.usePlatformThreads=false -Dlogback.configurationFile=logback-dev.xml -Dclojure.basis=.cpcache/3045479295.basis -Dclojure.compiler.direct-linking=true
# Blackhole mode: full + dont-inline hint
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Sampling time

bulkhead

This test was done with the (default) Resilience4J SemaphoreBulkhead. Fusebox also uses a semaphore for its bulkhead, and, as expected, the timings are very similar.

When using direct linking, Fusebox even slightly outperforms Resilience4J — likely due to having fewer layers of wrapping code.

Normal

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1545820.2058960.0513140.002354
Resilience4J0.1545820.1692670.0146850.001536
Fusebox (Parallel)0.3796825.4577515.0780690.031275
Resilience4J (Parallel)0.3796825.0257304.6460480.009122

Direct Linking

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1661320.1869270.0207950.001494
Resilience4J0.1661320.1672060.0010740.001602
Fusebox (Parallel)0.3640684.9192284.5551600.027115
Resilience4J (Parallel)0.3640685.3386094.9745410.012748

circuit-breaker

Circuit breaker was by far the most difficult utility to get right. Every other utility is more or less a wrapper on some java.util.concurrent class. Circuit breaker, on the other hand, is a novel clojure implementation. (Retry is also a novel re-write, but it's trivial to write.)

With that introduction, I'm very pleased with the results. With direct linking, Fusebox is within 20% of the overhead of Resilience4J for the most common use case, and is sub-microsecond for the single-threaded use case.

Normal

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1545820.4286990.2741170.005565
Resilience4J0.1545820.2412350.0866530.003138
Fusebox (Parallel)0.3796824.1281953.7485130.052430
Resilience4J (Parallel)0.3796822.0253331.6456510.057266

Direct Linking

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1661320.4337420.2676100.008181
Resilience4J0.1661320.2391930.0730610.001793
Fusebox (Parallel)0.3640683.5043393.1402710.017391
Resilience4J (Parallel)0.3640683.0134102.6493420.097159

fallback

Not much to say here. You're seeing the cost of adding a try/catch to a call.

Normal

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1545820.1856960.0311140.004278
Fusebox (Parallel)0.3796820.4059830.0263010.004382

Direct Linking

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1661320.1969840.0308520.007702
Fusebox (Parallel)0.3640680.4361530.0720850.014492

memoize

Memoize was tricky to do, and in some ways this test is useless. Ideally, it would have included a call to (Blackhole/consumeCPU 100) for every invocation, however to do so it would need a unique string value for every invocation. JMH makes it very clear that you shouldn't expect invocation-level fixtures to work well in the sub-millisecond range, so we're left more or less measuring the lookup speed to ConcurrentHashMap.

Normal

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1545820.064681-0.0899010.012942
Fusebox (Parallel)0.3796820.222143-0.1575390.008690

Direct Linking

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1661320.061700-0.1044320.008526
Fusebox (Parallel)0.3640680.232351-0.1317170.004692

rate-limit

The Rate Limit benchmark uses the (non-default) SemaphoreBasedRateLimiter for the Resilience4J test. The maintainer of Resilience4J says it is the highest throughput rate limiter they have, and it's the implementation Fusebox uses. Fusebox out-performs in the parallel benchmark, and my suspicion is it's because Resilience4J sets the fairness parameter to true for their rate limiter.

These benchmarks were done with the following settings:

  • Bucket Size (limit per period): 1000000
  • Period (limit refresh period): 1 ms

As stated in the introduction, the goal was for the rate limiter to never actually limit execution, and 1ns/execution was two orders of magnitude below baseline execution time.

Normal

TestBaselineAvg. Execution TimeApprox. OverheadError
Fusebox0.1545820.2057020.0511200.003154
Resilience4J0.1545820.1680280.0134460.001828
Fusebox (Parallel)0.3796823.5677763.1880940.025402
Resilience4J (Parallel)0.3796824.9807044.6010220.069383

Direct Linking

TestBaselineAvg. Execution TimeApprox. OverheadError
Fusebox0.1661320.1806840.0145520.005332
Resilience4J0.1661320.1733720.0072400.004391
Fusebox (Parallel)0.3640683.8711503.5070820.042465
Resilience4J (Parallel)0.3640685.2746534.9105850.076962

retry

It's not terribly surprising that Resilience4J outperforms Fusebox for serial execution. However, I was somewhat surprised to see a speed boost from Fusebox for parallel execution. My guess is that this is due to Resilience4J using shared LongAddrs to track successes and failures across all threads.

Normal

TestBaselineAvg. Execution TimeApprox. OverheadError
Fusebox0.1545820.2391820.0846000.005606
Resilience4J0.1545820.2133500.0587680.006528
Fusebox (Parallel)0.3796820.4158110.0361290.007256
Resilience4J (Parallel)0.3796820.4724250.0927430.019795

Direct Linking

TestBaselineAvg. Execution TimeApprox. OverheadError
Fusebox0.1661320.2325280.0663960.013922
Resilience4J0.1661320.2140290.0478970.007438
Fusebox (Parallel)0.3640680.3953920.0313240.005156
Resilience4J (Parallel)0.3640680.4354380.0713700.015799

timeout

Like bulkhead, timeout is more or less implemented the same in Fusebox and Resilience4J. Unsurprisingly, the numbers are basically identical, particularly once direct linking is turned on.

Normal

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1545824.9828394.8282570.026509
Resilience4J0.1545824.7180034.5634210.022608
Fusebox (Parallel)0.37968230.50855430.1288720.077493
Resilience4J (Parallel)0.37968229.21934128.8396590.075773

Direct Linking

TestBaseline (um)Avg. Execution Time (um)Approx. Overhead (um)Error (um)
Fusebox0.1661324.8061294.6399970.011052
Resilience4J0.1661324.6077094.4415770.011537
Fusebox (Parallel)0.36406829.74358829.3795200.078046
Resilience4J (Parallel)0.36406829.71517529.3511070.060138

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close