Builder Pattern Performance Optimization Guide
Overview
This guide provides best practices for optimizing builder pattern usage in Brobot for maximum performance.
Performance Considerations
1. Builder Instance Reuse
❌ Inefficient: Creating new builder for each modification
for (int i = 0; i < 1000; i++) {
ClickOptions options = ClickOptions.builder()
.setNumberOfClicks(i)
.build();
action.perform(options, collection);
}
✅ Efficient: Reusing builder with toBuilder()
ClickOptions baseOptions = ClickOptions.builder()
.setPressOptions(commonPressOptions)
.build();
for (int i = 0; i < 1000; i++) {
ClickOptions options = baseOptions.toBuilder()
.setNumberOfClicks(i)
.build();
action.perform(options, collection);
}
2. Lazy Initialization
❌ Eager initialization of all fields
public class ExpensiveOptions {
private final ExpensiveObject expensive = new ExpensiveObject(); // Always created
public static class Builder {
private ExpensiveObject expensive = new ExpensiveObject(); // Created even if not used
}
}
✅ Lazy initialization
public class ExpensiveOptions {
private final Supplier<ExpensiveObject> expensiveSupplier;
private ExpensiveObject expensive;
public ExpensiveObject getExpensive() {
if (expensive == null) {
expensive = expensiveSupplier.get();
}
return expensive;
}
public static class Builder {
private Supplier<ExpensiveObject> expensiveSupplier = ExpensiveObject::new;
public Builder setExpensiveSupplier(Supplier<ExpensiveObject> supplier) {
this.expensiveSupplier = supplier;
return this;
}
}
}
3. Immutable Object Caching
✅ Cache commonly used configurations
public class CommonConfigurations {
// Singleton instances for common configurations
public static final ClickOptions SINGLE_LEFT_CLICK = ClickOptions.builder()
.setNumberOfClicks(1)
.build();
public static final ClickOptions DOUBLE_CLICK = ClickOptions.builder()
.setNumberOfClicks(2)
.build();
public static final ClickOptions RIGHT_CLICK = ClickOptions.builder()
.setPressOptions(MousePressOptions.builder()
.setButton(MouseButton.RIGHT)
.build())
.build();
public static final PatternFindOptions QUICK_FIND = PatternFindOptions.builder()
.setStrategy(PatternFindOptions.Strategy.FIRST)
.setSimilarity(0.7)
.setCaptureImage(false)
.build();
// Thread-safe cache for dynamic configurations
private static final Map<String, ActionConfig> configCache =
new ConcurrentHashMap<>();
public static ActionConfig getCachedConfig(String key, Supplier<ActionConfig> builder) {
return configCache.computeIfAbsent(key, k -> builder.get());
}
}
4. Builder Pool Pattern
✅ For high-frequency builder usage
public class BuilderPool<T> {
private final Queue<T> pool = new ConcurrentLinkedQueue<>();
private final Supplier<T> factory;
private final Consumer<T> resetter;
private final int maxSize;
public BuilderPool(Supplier<T> factory, Consumer<T> resetter, int maxSize) {
this.factory = factory;
this.resetter = resetter;
this.maxSize = maxSize;
}
public T acquire() {
T builder = pool.poll();
return builder != null ? builder : factory.get();
}
public void release(T builder) {
if (pool.size() < maxSize) {
resetter.accept(builder);
pool.offer(builder);
}
}
}
// Usage
public class ClickOptionsBuilderPool {
private static final BuilderPool<ClickOptions.Builder> POOL =
new BuilderPool<>(
ClickOptions::builder,
builder -> {
// Reset to defaults
builder.setNumberOfClicks(1);
builder.setPressOptions(null);
builder.setVerification(null);
},
100 // max pool size
);
public static ClickOptions.Builder acquire() {
return POOL.acquire();
}
public static void release(ClickOptions.Builder builder) {
POOL.release(builder);
}
}
Memory Optimization
1. Null-Safe Defaults
❌ Creating unnecessary objects
public static class Builder {
private VerificationOptions verification = VerificationOptions.builder().build(); // Unnecessary object
private RepetitionOptions repetition = RepetitionOptions.builder().build(); // Unnecessary object
}
✅ Lazy creation only when needed
public static class Builder {
private VerificationOptions.VerificationOptionsBuilder verification;
private RepetitionOptions.RepetitionOptionsBuilder repetition;
public Builder setVerification(VerificationOptions.VerificationOptionsBuilder verification) {
this.verification = verification;
return this;
}
public ClickOptions build() {
return new ClickOptions(
verification != null ? verification.build() : null,
repetition != null ? repetition.build() : null
);
}
}
2. Primitive vs Object Fields
✅ Use primitives where possible
public static class Builder {
private int numberOfClicks = 1; // primitive
private double pauseAfter = 0.0; // primitive
private boolean captureImage = false; // primitive
// Instead of
// private Integer numberOfClicks = 1; // boxed
// private Double pauseAfter = 0.0; // boxed
// private Boolean captureImage = false; // boxed
}
JVM Optimizations
1. Method Inlining
✅ Keep setter methods simple for JVM inlining
public Builder setNumberOfClicks(int clicks) {
this.numberOfClicks = clicks;
return this;
}
❌ Complex setters prevent inlining
public Builder setNumberOfClicks(int clicks) {
if (clicks < 0) {
logger.warn("Negative clicks: " + clicks);
this.numberOfClicks = 1;
} else if (clicks > 10) {
logger.warn("Too many clicks: " + clicks);
this.numberOfClicks = 10;
} else {
this.numberOfClicks = clicks;
}
logMetrics(clicks);
validateState();
return this;
}
2. Final Fields
✅ Mark fields final in immutable objects
public final class ClickOptions {
private final int numberOfClicks;
private final MousePressOptions mousePressOptions;
// JVM can optimize final fields better
}
Benchmarking
Performance Test Template
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BuilderBenchmark {
@Benchmark
public ClickOptions directBuilder() {
return ClickOptions.builder()
.setNumberOfClicks(2)
.build();
}
@Benchmark
public ClickOptions cachedBuilder() {
return CommonConfigurations.DOUBLE_CLICK;
}
@Benchmark
public ClickOptions pooledBuilder() {
ClickOptions.Builder builder = ClickOptionsBuilderPool.acquire();
try {
return builder.setNumberOfClicks(2).build();
} finally {
ClickOptionsBuilderPool.release(builder);
}
}
}
Expected Results
Benchmark Mode Cnt Score Error Units
BuilderBenchmark.directBuilder avgt 10 145.234 ± 3.456 ns/op
BuilderBenchmark.cachedBuilder avgt 10 2.345 ± 0.123 ns/op
BuilderBenchmark.pooledBuilder avgt 10 45.678 ± 2.345 ns/op
Anti-Patterns to Avoid
1. Builder Mutation After Build
// ❌ WRONG - Builder should not be used after build()
ClickOptions.Builder builder = ClickOptions.builder();
ClickOptions options1 = builder.setNumberOfClicks(1).build();
ClickOptions options2 = builder.setNumberOfClicks(2).build(); // Unsafe!
2. Excessive Builder Nesting
// ❌ Too much nesting reduces readability
ClickOptions options = ClickOptions.builder()
.setPressOptions(MousePressOptions.builder()
.setButton(MouseButton.LEFT)
.setPauseBeforeMouseDown(0.1)
.setPauseAfterMouseDown(0.1)
.setPauseBeforeMouseUp(0.1)
.setPauseAfterMouseUp(0.1)
.build())
.setVerification(VerificationOptions.builder()
.setEvent(Event.TEXT_APPEARS)
.setText("Success")
.setObjectCollection(ObjectCollection.builder()
.addStateImage(StateImage.builder()
.setName("image1")
.build())
.build())
.build())
.build();
// ✅ Better - Extract complex builders
MousePressOptions pressOptions = createPressOptions();
VerificationOptions verification = createVerification();
ClickOptions options = ClickOptions.builder()
.setPressOptions(pressOptions)
.setVerification(verification)
.build();
3. Thread Safety Issues
// ❌ Shared mutable builder
public class Service {
private final ClickOptions.Builder sharedBuilder = ClickOptions.builder(); // NOT thread-safe
public void performClick(int clicks) {
ClickOptions options = sharedBuilder
.setNumberOfClicks(clicks) // Race condition!
.build();
}
}
// ✅ Thread-safe approach
public class Service {
public void performClick(int clicks) {
ClickOptions options = ClickOptions.builder() // New builder per invocation
.setNumberOfClicks(clicks)
.build();
}
}
Memory Profiling
Using JVM Flags
# Track object allocation
-XX:+PrintGC -XX:+PrintGCDetails
# Memory usage analysis
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# Heap dump on OOM
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof
Profiling Code
@Test
public void profileBuilderMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
// Warm-up
for (int i = 0; i < 1000; i++) {
ClickOptions.builder().setNumberOfClicks(i).build();
}
System.gc();
long memBefore = runtime.totalMemory() - runtime.freeMemory();
// Test
List<ClickOptions> options = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
options.add(ClickOptions.builder()
.setNumberOfClicks(i % 10)
.build());
}
long memAfter = runtime.totalMemory() - runtime.freeMemory();
long memUsed = memAfter - memBefore;
System.out.printf("Memory used: %.2f MB for %d objects (%.2f bytes/object)%n",
memUsed / 1024.0 / 1024.0,
options.size(),
(double) memUsed / options.size());
}
Best Practices Summary
- Cache immutable configurations that are used frequently
- Use toBuilder() for variations of existing configurations
- Avoid unnecessary object creation in builders
- Use primitives instead of boxed types where possible
- Keep setter methods simple for JVM inlining
- Extract complex builders to improve readability
- Never share mutable builders between threads
- Profile memory usage in performance-critical paths
- Use builder pools for high-frequency scenarios
- Document performance characteristics in JavaDoc