/* * SPDX-License-Identifier: MIT */ package ta4jexamples.rules; import java.text.NumberFormat; import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.ta4j.core.Rule; import org.ta4j.core.TradingRecord; import org.ta4j.core.rules.AbstractRule; import org.ta4j.core.rules.AndRule; import org.ta4j.core.rules.FixedRule; /** * Comparative benchmark for eager vs lazy rule naming. *

* Scenarios: *

* * @since 0.22.0 */ public class RuleNameBenchmark { private static final Logger LOG = LogManager.getLogger(RuleNameBenchmark.class); private static final int DEFAULT_THREADS = Math.max(8, Runtime.getRuntime().availableProcessors()); private static final int DEFAULT_RULES_PER_THREAD = 100_000; private static final int DEFAULT_BATCHES = 3; public static void main(String[] args) throws Exception { int threads = args.length > 0 ? Integer.parseInt(args[0]) : DEFAULT_THREADS; int batches = args.length > 1 ? Integer.parseInt(args[1]) : DEFAULT_BATCHES; int rulesPerThread = args.length > 2 ? Integer.parseInt(args[2]) : DEFAULT_RULES_PER_THREAD; new RuleNameBenchmark().run(threads, batches, rulesPerThread); } private void run(int threads, int batches, int rulesPerThread) throws Exception { String formattedRulesPerThread = NumberFormat.getIntegerInstance(Locale.US).format(rulesPerThread); LOG.info("Starting rule construction benchmark: threads={}, batches={}, rulesPerThread={}", threads, batches, formattedRulesPerThread); Map statsByScenario = new HashMap<>(); for (int batch = 1; batch <= batches; batch++) { runScenario("Eager setName() + getName()", threads, rulesPerThread, this::buildEagerRule, true, batch, statsByScenario); runScenario("Lazy setName() + getName()", threads, rulesPerThread, this::buildLazyRule, true, batch, statsByScenario); runScenario("Eager setName() + No getName()", threads, rulesPerThread, this::buildEagerRuleNoChildNames, false, batch, statsByScenario); } LOG.info("=== Rule construction throughput summary (threads={}, batches={}, rulesPerThread={}) ===", threads, batches, formattedRulesPerThread); statsByScenario.forEach((label, s) -> { double avgThroughput = s.totalThroughput / s.runs; Duration avgDuration = Duration.ofNanos((long) (s.totalDurationNanos / s.runs)); LOG.info( "{}: runs={}, avgThroughput={} rules/s, minThroughput={}, maxThroughput={}, avgDuration={}, totalChecksum={}", label, s.runs, NumberFormat.getIntegerInstance(Locale.US).format(avgThroughput), NumberFormat.getIntegerInstance(Locale.US).format(s.minThroughput), NumberFormat.getIntegerInstance(Locale.US).format(s.maxThroughput), avgDuration, s.totalChecksum); }); } private void runScenario(String label, int threads, int rulesPerThread, Supplier factory, boolean callName, int batch, Map statsByScenario) throws Exception { long started = System.nanoTime(); long checksum = exercise(threads, rulesPerThread, factory, callName); long elapsedNanos = System.nanoTime() - started; double elapsedSeconds = elapsedNanos / 1_000_000_000.0; double constructions = (double) threads * rulesPerThread; double throughput = constructions / elapsedSeconds; LOG.debug("Batch {} [{}]: duration={}, throughput={} rules/s, checksum={}", batch, label, Duration.ofNanos(elapsedNanos), NumberFormat.getIntegerInstance(Locale.US).format(throughput), checksum); ScenarioStats stats = statsByScenario.computeIfAbsent(label, k -> new ScenarioStats()); stats.runs++; stats.totalDurationNanos += elapsedNanos; stats.totalThroughput += throughput; stats.totalChecksum += checksum; stats.minThroughput = Math.min(stats.minThroughput, throughput); stats.maxThroughput = Math.max(stats.maxThroughput, throughput); } private long exercise(int threads, int rulesPerThread, Supplier factory, boolean callName) throws Exception { var pool = Executors.newFixedThreadPool(threads); try { List> futures = new ArrayList<>(threads); for (int t = 0; t < threads; t++) { futures.add(CompletableFuture.supplyAsync(() -> { long localChecksum = 0; for (int i = 0; i < rulesPerThread; i++) { Rule r = factory.get(); if (callName) { String name = r.getName(); localChecksum += name.hashCode(); } else { // Touch type to prevent dead-code elimination without invoking getName() localChecksum += r.getClass().hashCode(); } } return localChecksum; }, pool)); } long total = 0; for (CompletableFuture future : futures) { total += future.get(5, TimeUnit.MINUTES); } return total; } finally { pool.shutdown(); pool.awaitTermination(30, TimeUnit.SECONDS); } } private Rule buildEagerRule() { Rule left = new FixedRule(1); Rule right = new FixedRule(2); return new AndRule(left, right); } Rule buildLazyRule() { Rule left = new FixedRule(1); Rule right = new FixedRule(2); return new LazyAndRule(left, right); } Rule buildEagerRuleNoChildNames() { Rule left = new FixedRule(1); Rule right = new FixedRule(2); return new NoNameLeakAndRule(left, right); } /** * Lazy variant of AndRule that defers name construction to getName(). */ static final class LazyAndRule extends AbstractRule { private final Rule rule1; private final Rule rule2; LazyAndRule(Rule rule1, Rule rule2) { this.rule1 = rule1; this.rule2 = rule2; } @Override public boolean isSatisfied(int index, TradingRecord tradingRecord) { boolean satisfied = rule1.isSatisfied(index, tradingRecord) && rule2.isSatisfied(index, tradingRecord); traceIsSatisfied(index, satisfied); return satisfied; } @Override protected String createDefaultName() { setName(createCompositeName(getClass().getSimpleName(), rule1, rule2)); return getName(); } } /** * Eager variant that constructs the name in the constructor but avoids calling * getName() on children (to prevent triggering their lazy name resolution). * This demonstrates that even when setName() is called eagerly, if getName() is * never invoked on the rule, the JIT compiler may be able to optimize away some * of the string construction overhead, though the volatile write itself cannot * be eliminated. *

* Note: The volatile write in setName() is an observable side effect that * prevents complete dead-code elimination, but the string construction work * (StringBuilder operations) may still be optimized if the result is never * read. */ static final class NoNameLeakAndRule extends AbstractRule { private final Rule rule1; private final Rule rule2; NoNameLeakAndRule(Rule rule1, Rule rule2) { this.rule1 = rule1; this.rule2 = rule2; // Eagerly construct and set the name, but use class names instead of // calling getName() on children to avoid triggering their lazy name resolution. // This allows us to test if JIT can eliminate the string construction // overhead when the name is never accessed. setName(buildNameWithoutChildNames()); } @Override public boolean isSatisfied(int index, TradingRecord tradingRecord) { boolean satisfied = rule1.isSatisfied(index, tradingRecord) && rule2.isSatisfied(index, tradingRecord); traceIsSatisfied(index, satisfied); return satisfied; } private String buildNameWithoutChildNames() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()); builder.append('('); builder.append(rule1 == null ? "null" : rule1.getClass().getSimpleName()); builder.append(','); builder.append(rule2 == null ? "null" : rule2.getClass().getSimpleName()); builder.append(')'); return builder.toString(); } } private static final class ScenarioStats { int runs; double totalDurationNanos; double totalThroughput; double minThroughput = Double.MAX_VALUE; double maxThroughput = Double.MIN_VALUE; long totalChecksum; } }