mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
7.7 KiB
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:
- Which approach is faster? (Loop styles, data structures)
- Will this scale? (200 records vs 10,000)
- 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:
- Processing 200+ records regularly
- Approaching governor limits
- User experience is affected
- Benchmarks show measurable improvement
Related Resources
- assets/benchmarking-template.cls - Ready-to-use benchmark template
- assets/cpu-heap-optimization.cls - Optimization patterns
- Apex Log Analyzer - Visual performance analysis