Hi all,

Here are the JMH numbers I promised. JMH 1.37, JDK 21.0.11, Apple M4 Pro
laptop (8P+4E cores), camel 4.24.0-SNAPSHOT;
3 forks, 5 x 1 s warmup, 10 x 1 s measurement, average time in ns/op. A
laptop result, so indicative only.

                                      1 thread          8 threads
  System.nanoTime()                   8.92 +- 0.01      356.1 +- 3.3
  TimeSource.nanoTime(), volatile     9.10 +- 0.06      354.9 +- 3.2
  TimeSource.nanoTime(), plain        9.15 +- 0.08      358.0 +- 3.3
  TimeSource.nanoTime(), bimorphic    9.22 +- 0.13      355.7 +- 2.8
  System.currentTimeMillis()         13.59 +- 0.09      186.5 +- 7.8
  TimeSource.currentTimeMillis()     13.62 +- 0.02      185.2 +- 9.5
  new StopWatch().taken(), today     19.96 +- 0.04      709.6 +- 7.6
  the same with TimeSource           20.52 +- 0.12      710.8 +- 7.8

  direct:a -> log:x?level=OFF, route statistics on (today's core, for
scale):
                                      2.43M msg/s       10.6M msg/s (8
threads)

"bimorphic" means a second provider class was installed and used once in
setup, then SYSTEM was put back, so the
call site has seen two receiver types. With 8 threads the clock calls are
much slower on this machine for every
variant, System.nanoTime() included; I have not dug into why, but the
variants are the same there.

What it means: on one thread the indirection costs about 0.2 ns per
nanoTime() call and about 0.5 ns per StopWatch,
nothing measurable for currentTimeMillis(), and nothing measurable with 8
threads. The volatile field is not
measurably slower than a plain static field here (9.10 vs 9.15 ns), so I
would keep volatile, which also keeps
install() safe without extra rules. Against about 410 ns per message for
the simplest direct -> log route, a few
tenths of a nanosecond per clock read is well below noise. On x86 a
volatile read is also a plain load, but
I have only measured on AArch64; numbers from an x86 box would be welcome.

I can put the benchmark in camel-performance-tests if useful.

Thanks,
Shashank

On Mon, 19 Oct 2026 at 12:44, Shashank Jain <[email protected]>
wrote:

>
> Hi all,
>
> camel-test-dst (CAMEL-25738, PR https://github.com/apache/camel/pull/28067)
> runs the real consumer code of a
> component under one seeded scheduler with virtual time, so a test can try
> thousands of interleavings and faults in
> seconds and replay a failing seed exactly. Worlds written with it found or
> confirmed several timing bugs, for
> example CAMEL-25683, CAMEL-25737, CAMEL-25741, CAMEL-25756 and CAMEL-25758.
>
> The harness controls Camel's thread pools through the existing
> ThreadPoolFactory SPI. Time is the gap: core reads
> System directly, so today a test either rewrites the byte code of core
> classes or lives with real time and real
> sleeps in core. Claus suggested a small time source in camel-util, and I
> would like to discuss that here before
> anything goes into core.
>
> Where core reads the clock or sleeps today (src/main/java on main,
> 2026-10-17):
>
>   camel-util            4 currentTimeMillis, 5 nanoTime, 3 Thread.sleep
>   camel-support        24 currentTimeMillis, 6 nanoTime, 6 Thread.sleep, 5
> Instant.now
>   camel-base           43 currentTimeMillis (nearly all event timestamps)
>   camel-base-engine     9 currentTimeMillis, 2 Thread.sleep, 1
> Instant.now, 1 new Date()
>   camel-core-processor  6 currentTimeMillis, 7 nanoTime, 5 Thread.sleep
>   camel-management      9 currentTimeMillis, 1 Thread.sleep, 2 new Date()
>   others (api, languages, console, health, main): a handful
>
> About 30 of these change behaviour, and only those need to be controlled
> in a test:
> - waits: RedeliveryPolicy.sleep and the sleep loop of
> RedeliveryErrorHandler, ForegroundTask, the wait loop of
>   DefaultShutdownStrategy, DelayProcessorSupport, MulticastProcessor,
> OptimisticLockRetryPolicy,
>   DefaultStartupConditionStrategy;
> - task timing: StopWatch, MonotonicClock, AbstractTask, BackgroundTask,
> TimeBoundedBudget, BackOffTimerTask,
>   DefaultTimeoutMap;
> - throttling: TotalRequestsThrottler, ConcurrentRequestsThrottler,
> SamplingThrottler,
>   ThrottlingExceptionRoutePolicy.
>
> Event timestamps, the debugger, the tracer, management and the consoles
> can stay on System.
>
> Options I see:
>
> 1. Byte code rewriting only (what stage 1 does). No core change, but
> runtime agent attach is being phased out in the
>    JDK, it only covers the calls it is told about, and rewritten core
> classes stay rewritten for the whole test JVM.
>
> 2. A time source for all of core: about 120 call sites, many on hot paths,
> for no gain in tests.
>
> 3. In between, which is what I would propose:
>    - a small static, internal TimeSource in camel-util:
> currentTimeMillis(), nanoTime(), instant(), sleep(long).
>      The default delegates to System and Thread.sleep; a test harness can
> install another provider. Static rather
>      than per CamelContext, because StopWatch and MonotonicClock have no
> context;
>    - move only the ~30 call sites above;
>    - keep byte code rewriting for the component under test, pointed at
> TimeSource, with the agent loaded by
>      -javaagent instead of attached at runtime;
>    - later, components use it when they are touched anyway, and a
> forbidden-apis rule keeps new
>      Instant.now() / Thread.sleep() out of consumers.
>
> Rough shape:
>
>   public final class TimeSource {
>       public interface Provider {
>           long currentTimeMillis();
>           long nanoTime();
>           void sleep(long millis) throws InterruptedException;
>       }
>       private static volatile Provider provider = SYSTEM;
>       public static long nanoTime() { return provider.nanoTime(); }
>       ...
>       public static Provider install(Provider p) { ... }  // test
> harnesses only
>   }
>
> Open points: package and name; a volatile field or a plain static field
> set once before the context starts; and
> whether sleep belongs there or in a separate wait helper.
>
> On cost: I will run a JMH benchmark (direct System.nanoTime vs. TimeSource
> with a volatile and a plain field,
> StopWatch, and a direct -> log route with statistics on) and post the
> numbers in this thread. My expectation is
> that the default path inlines to the same call plus one ordinary load, so
> it should be within noise.
>
> Thoughts?
>
> Thanks,
> Shashank
>

Reply via email to