afv-library/skills/platform-apex-logs-debug/references/benchmarking-guide.md

7.7 KiB

Apex Benchmarking Guide

Performance testing is essential for writing efficient Apex code. This guide covers reliable benchmarking techniques and real-world performance data based on established Apex community practices and Salesforce governor limit behavior.


Why Benchmark?

"Premature optimization is the root of all evil" - but informed optimization is essential. Benchmarking answers:

  1. Which approach is faster? (Loop styles, data structures)
  2. Will this scale? (200 records vs 10,000)
  3. Where are the bottlenecks? (CPU, heap, SOQL)

Apex Benchmarking Technique

The recommended approach for reliable Apex performance testing:

The Pattern

// Run in Anonymous Apex for consistent environment
Long startTime = System.currentTimeMillis();

// Your code to benchmark
for (Integer i = 0; i < 10000; i++) {
    // Operation being tested
}

Long endTime = System.currentTimeMillis();
System.debug('Duration: ' + (endTime - startTime) + 'ms');

Key Principles

Principle Why It Matters
Use Anonymous Apex Consistent execution environment, no trigger interference
Run Multiple Iterations Averages out JIT compilation and garbage collection
Test at Scale 200 records ≠ 10,000 records in terms of performance
Isolate the Operation Test one thing at a time
Run Multiple Times First run often slower due to JIT compilation

Example: Complete Benchmark

// Comprehensive benchmark template
public class BenchmarkRunner {

    public static void compareMethods() {
        Integer iterations = 10000;

        // Warm-up run (JIT compilation)
        warmUp();

        // Method A
        Long startA = System.currentTimeMillis();
        for (Integer i = 0; i < iterations; i++) {
            methodA();
        }
        Long durationA = System.currentTimeMillis() - startA;

        // Method B
        Long startB = System.currentTimeMillis();
        for (Integer i = 0; i < iterations; i++) {
            methodB();
        }
        Long durationB = System.currentTimeMillis() - startB;

        // Results
        System.debug('Method A: ' + durationA + 'ms');
        System.debug('Method B: ' + durationB + 'ms');
        System.debug('Difference: ' + Math.abs(durationA - durationB) + 'ms');
        System.debug('Winner: ' + (durationA < durationB ? 'Method A' : 'Method B'));
    }

    private static void warmUp() {
        for (Integer i = 0; i < 100; i++) {
            methodA();
            methodB();
        }
    }

    private static void methodA() { /* Implementation A */ }
    private static void methodB() { /* Implementation B */ }
}

Real-World Benchmark Results

String Concatenation vs String.join()

From documented Apex benchmarks:

Method Records Duration Result
String += in loop 1,750 11,767ms CPU LIMIT HIT
String.join() 7,500 539ms Still running
Improvement - 22x faster -
// ❌ SLOW: String concatenation in loop (O(n²) string copies)
String result = '';
for (Account acc : accounts) {
    result += acc.Name + '\n';  // Creates new string each time!
}

// ✅ FAST: String.join() (O(n) single allocation)
List<String> names = new List<String>();
for (Account acc : accounts) {
    names.add(acc.Name);
}
String result = String.join(names, '\n');

Loop Performance Comparison

From documented Apex loop benchmarks (10,000 iterations):

Loop Type Duration Notes
While loop ~0.4s Fastest
Cached iterator ~0.8s Good alternative
For loop (index) ~1.4s Acceptable
Enhanced for-each ~2.4s Convenient but slower
Uncached iterator CPU LIMIT Avoid
// 🏆 FASTEST: While loop
Iterator<Account> iter = accounts.iterator();
while (iter.hasNext()) {
    Account acc = iter.next();
    // process
}

// ✅ GOOD: Traditional for loop
for (Integer i = 0; i < accounts.size(); i++) {
    Account acc = accounts[i];
    // process
}

// ⚠️ CONVENIENT BUT SLOWER: Enhanced for-each
for (Account acc : accounts) {
    // process - OK for small collections
}

Map vs List Lookup

Operation Complexity 10,000 lookups
List.contains() O(n) ~500ms
Set.contains() O(1) ~5ms
Map.containsKey() O(1) ~5ms
// ❌ SLOW: List lookup
List<Id> processedIds = new List<Id>();
for (Account acc : accounts) {
    if (!processedIds.contains(acc.Id)) {  // O(n) each time!
        processedIds.add(acc.Id);
    }
}

// ✅ FAST: Set lookup
Set<Id> processedIds = new Set<Id>();
for (Account acc : accounts) {
    if (!processedIds.contains(acc.Id)) {  // O(1) constant time
        processedIds.add(acc.Id);
    }
}

Governor Limit Ceilings

Official Limits

Limit Synchronous Asynchronous
CPU Time 10,000 ms 60,000 ms
Heap Size 6 MB 12 MB
SOQL Queries 100 200
DML Statements 150 150

Practical Thresholds

Limit Warning (80%) Critical (95%)
CPU Time 8,000 ms 9,500 ms
Heap Size 4.8 MB 5.7 MB
SOQL Queries 80 95

Runtime Limit Checking

public void processWithSafety(List<Account> accounts) {
    Integer cpuWarning = 8000;
    Integer heapWarning = 4800000;

    for (Account acc : accounts) {
        // Check before each operation
        if (Limits.getCpuTime() > cpuWarning) {
            System.debug(LoggingLevel.WARN,
                'CPU approaching limit: ' + Limits.getCpuTime() + 'ms');
            // Consider switching to async or chunking
            break;
        }

        if (Limits.getHeapSize() > heapWarning) {
            System.debug(LoggingLevel.WARN,
                'Heap approaching limit: ' + Limits.getHeapSize() + ' bytes');
            break;
        }

        processAccount(acc);
    }
}

Benchmarking Anti-Patterns

Don't Do These

Anti-Pattern Problem Solution
Testing in triggers Inconsistent environment Use Anonymous Apex
Single iteration JIT variance affects results Run 1000+ iterations
Testing with 10 records Doesn't reveal O(n²) issues Test with 200+ records
Ignoring warm-up First run skewed by JIT Add warm-up phase
Mixing operations Can't isolate bottleneck Test one thing at a time

Benchmarking Checklist

Before optimizing, verify:

  • Ran benchmark multiple times (3-5 runs)
  • Used 1000+ iterations for micro-benchmarks
  • Tested with production-scale data (200+ records)
  • Included warm-up phase
  • Ran in Anonymous Apex (not test context)
  • Compared both approaches fairly
  • Considered readability trade-offs

When NOT to Optimize

Sometimes clarity beats performance:

// More readable, negligible performance difference for small collections
for (Account acc : accounts) {
    acc.Description = acc.Name + ' - Updated';
}

// vs micro-optimized but harder to read
Iterator<Account> iter = accounts.iterator();
while (iter.hasNext()) {
    Account acc = iter.next();
    acc.Description = acc.Name + ' - Updated';
}

Rule of thumb: Optimize when:

  1. Processing 200+ records regularly
  2. Approaching governor limits
  3. User experience is affected
  4. Benchmarks show measurable improvement