
Platform Apex Logs Debug
- 2.4k installs
- 763 repo stars
- Updated July 24, 2026
- forcedotcom/sf-skills
Retrieves and analyzes Salesforce Apex debug logs so a solo builder can trace errors and understand what happened during a transaction in an org.
About
An official Salesforce skill that retrieves and helps analyze Apex debug logs from an org so a developer can trace what happened during a transaction and pinpoint errors. A solo builder reaches for it when Apex code misbehaves in a Salesforce org and they need to read the debug logs to diagnose the failure instead of manually pulling logs from setup.
- Fetches Salesforce Apex debug logs
- Helps trace errors and execution
- Speeds up Apex debugging
- Official forcedotcom skill set
Platform Apex Logs Debug by the numbers
- 2,433 all-time installs (skills.sh)
- +627 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #35 of 610 Debugging skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/forcedotcom/sf-skills --skill platform-apex-logs-debugAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.4k |
|---|---|
| repo stars | ★ 763 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | forcedotcom/sf-skills ↗ |
What it does
Retrieves and analyzes Salesforce Apex debug logs so a solo builder can trace errors and understand what happened during a transaction in an org.
Who is it for?
Debugging Apex issues in a Salesforce org
Skip if: Non-Salesforce debugging
What you get
- retrieved and analyzed debug logs
Files
platform-apex-logs-debug: Salesforce Debug Log Analysis & Troubleshooting
Use this skill when the user needs root-cause analysis from debug logs: governor-limit diagnosis, stack-trace interpretation, slow-query investigation, heap / CPU pressure analysis, or a reproduction-to-fix loop based on log evidence.
When This Skill Owns the Task
Use platform-apex-logs-debug when the work involves:
.logfiles from Salesforce- stack traces and exception analysis
- governor limits
- SOQL / DML / CPU / heap troubleshooting
- query-plan or performance evidence extracted from logs
Delegate elsewhere when the user is:
- running or repairing Apex tests → platform-apex-test-run
- generating or implementing the code fix → platform-apex-generate
- debugging Agentforce session traces / parquet telemetry → agentforce-observe
---
Required Context to Gather First
Ask for or infer:
- org alias
- failing transaction / user flow / test name
- approximate timestamp or transaction window
- user / record / request ID if known
- whether the goal is diagnosis only or diagnosis + fix loop
---
Recommended Workflow
1. Retrieve logs
Use the commands in references/cli-commands.md to list, download, or stream logs for the target org.
2. Analyze in this order
1. entry point and transaction type 2. exceptions / fatal errors 3. governor limits 4. repeated SOQL / DML patterns 5. CPU / heap hotspots 6. callout timing and external failures
3. Classify severity
- Critical — runtime failure, hard limit, corruption risk
- Warning — near-limit, non-selective query, slow path
- Info — optimization opportunity or hygiene issue
4. Recommend the smallest correct fix
Prefer fixes that are:
- root-cause oriented
- bulk-safe
- testable
- easy to verify with a rerun
Expanded workflow: references/analysis-playbook.md
---
High-Signal Issue Patterns
| Issue | Primary signal | Default fix direction |
|---|---|---|
| SOQL in loop | repeating SOQL_EXECUTE_BEGIN in a repeated call path | query once, use maps / grouped collections |
| DML in loop | repeated DML_BEGIN patterns | collect rows, bulk DML once |
| Non-selective query | high rows scanned / poor selectivity | add indexed filters, reduce scope |
| CPU pressure | CPU usage approaching sync limit | reduce algorithmic complexity, cache, async where valid |
| Heap pressure | heap usage approaching sync limit | stream with SOQL for-loops, reduce in-memory data |
| Null pointer / fatal error | EXCEPTION_THROWN / FATAL_ERROR | guard null assumptions, fix empty-query handling |
Expanded examples: references/common-issues.md
---
Output Format
When finishing analysis, report in this order:
1. What failed 2. Where it failed (class / method / line / transaction stage) 3. Why it failed (root cause, not just symptom) 4. How severe it is 5. Recommended fix 6. Verification step
Suggested shape:
Issue: <summary>
Location: <class / line / transaction>
Root cause: <explanation>
Severity: Critical | Warning | Info
Fix: <specific action>
Verify: <test or rerun step>---
Rules / Constraints
| Rule | Rationale |
|---|---|
| Always base fix recommendations on log evidence | Avoid speculative diagnosis — root cause must be traceable in the log |
| Report all six output fields for every issue found | Ensures actionable, complete findings for each problem |
| Classify every finding as Critical, Warning, or Info | Helps the user prioritize which issues to address first |
Delegate code generation to platform-apex-generate | This skill diagnoses; it does not rewrite Apex code |
Delegate test execution to platform-apex-test-run | This skill does not run or repair test classes |
Never assume limits are safe without reading LIMIT_USAGE events | Limits may be consumed by earlier operations not visible in the failure point |
---
Gotchas
| Pitfall | Resolution |
|---|---|
| Log truncated at 2 MB | Reduce debug levels (e.g., ApexCode: INFO, ApexProfiling: FINE) and re-capture |
| Same issue appears as both SOQL and CPU problem | Fix SOQL-in-loop first — it typically drives the CPU spike as a secondary effect |
| No logs appear after trace flag is set | Verify the trace flag ExpirationDate is in the future and the correct user is traced |
| Async context changes limit values | CPU limit is 60,000 ms async vs 10,000 ms sync — check transaction type before flagging limits |
| Stack trace points to framework line, not user code | Walk up the call stack past trigger handlers to find the originating user code |
---
Cross-Skill Integration
| Need | Delegate to | Reason |
|---|---|---|
| Implement Apex fix | platform-apex-generate | code change generation / review |
| Reproduce via tests | platform-apex-test-run | test execution and coverage loop |
| Deploy fix | platform-metadata-deploy | deployment orchestration |
| Create debugging data | platform-data-manage | targeted seed / repro data |
---
Reference File Index
| File | When to read |
|---|---|
references/analysis-playbook.md | Start here — expanded step-by-step workflow for any debugging session |
references/common-issues.md | Quick lookup for SOQL in loop, DML in loop, CPU/heap pressure, null pointer patterns |
references/cli-commands.md | SF CLI commands for retrieving, streaming, and managing debug logs |
references/debug-log-reference.md | Full event type catalog, log levels, and governor limit reference values |
references/log-analysis-tools.md | Tool guide: Apex Log Analyzer, Developer Console, CLI grep patterns |
references/benchmarking-guide.md | Performance benchmarking techniques, benchmark data, and anti-patterns |
references/scoring-rubric.md | 100-point scoring rubric for evaluating analysis quality |
assets/benchmarking-template.cls | Copy-paste Anonymous Apex template for running performance benchmarks |
assets/cpu-heap-optimization.cls | Apex patterns for reducing CPU time and heap allocation |
assets/dml-in-loop-fix.cls | Before/after example for resolving DML-in-loop violations |
assets/soql-in-loop-fix.cls | Before/after example for resolving SOQL-in-loop violations |
assets/null-pointer-fix.cls | Patterns for guarding against null pointer exceptions |
---
Score Guide
| Score | Meaning |
|---|---|
| 90+ | Expert analysis with strong fix guidance |
| 80–89 | Good analysis with minor gaps |
| 70–79 | Acceptable but may miss secondary issues |
| 60–69 | Partial diagnosis only |
| < 60 | Incomplete analysis |
/**
* Apex Benchmarking Template
*
* Reliable performance testing using established Apex benchmarking techniques.
* Run in Anonymous Apex for consistent results.
*
* Features:
* - Warm-up phase to normalize JIT compilation
* - Multiple iterations for statistical accuracy
* - Side-by-side comparison of two approaches
* - Automatic winner determination
*
* See: references/benchmarking-guide.md for methodology and benchmark data
*/
// ═══════════════════════════════════════════════════════════════════════════
// SIMPLE BENCHMARK (Copy into Anonymous Apex)
// ═══════════════════════════════════════════════════════════════════════════
// Quick single-operation benchmark
Long startTime = System.currentTimeMillis();
for (Integer i = 0; i < 10000; i++) {
// Your operation here
String s = 'test'.toUpperCase();
}
Long duration = System.currentTimeMillis() - startTime;
System.debug('Duration: ' + duration + 'ms');
System.debug('Per iteration: ' + (duration / 10000.0) + 'ms');
// ═══════════════════════════════════════════════════════════════════════════
// COMPARISON BENCHMARK CLASS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Compare two implementations side-by-side
*
* Usage in Anonymous Apex:
* BenchmarkComparison.run();
*/
public class BenchmarkComparison {
private static final Integer ITERATIONS = 10000;
private static final Integer WARMUP_ITERATIONS = 100;
public static void run() {
System.debug('═══════════════════════════════════════════════════════');
System.debug('BENCHMARK: [Description Here]');
System.debug('Iterations: ' + ITERATIONS);
System.debug('═══════════════════════════════════════════════════════');
// Warm-up phase (normalizes JIT compilation effects)
System.debug('\n🔥 Warm-up phase...');
for (Integer i = 0; i < WARMUP_ITERATIONS; i++) {
methodA();
methodB();
}
System.debug(' Warm-up complete');
// Benchmark Method A
System.debug('\n📊 Running Method A...');
Long startA = System.currentTimeMillis();
for (Integer i = 0; i < ITERATIONS; i++) {
methodA();
}
Long durationA = System.currentTimeMillis() - startA;
// Benchmark Method B
System.debug('📊 Running Method B...');
Long startB = System.currentTimeMillis();
for (Integer i = 0; i < ITERATIONS; i++) {
methodB();
}
Long durationB = System.currentTimeMillis() - startB;
// Results
System.debug('\n═══════════════════════════════════════════════════════');
System.debug('RESULTS');
System.debug('═══════════════════════════════════════════════════════');
System.debug('Method A: ' + durationA + 'ms (' + (durationA / (Decimal)ITERATIONS) + 'ms per iteration)');
System.debug('Method B: ' + durationB + 'ms (' + (durationB / (Decimal)ITERATIONS) + 'ms per iteration)');
System.debug('───────────────────────────────────────────────────────');
Long difference = Math.abs(durationA - durationB);
String winner = durationA < durationB ? 'Method A' : 'Method B';
Decimal improvement = durationA < durationB
? (durationB / (Decimal)durationA)
: (durationA / (Decimal)durationB);
System.debug('🏆 Winner: ' + winner);
System.debug(' Difference: ' + difference + 'ms');
System.debug(' Improvement: ' + improvement.setScale(1) + 'x faster');
// Governor limits status
System.debug('\n📈 Governor Limits Used:');
System.debug(' CPU Time: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime() + 'ms');
System.debug(' Heap Size: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize() + ' bytes');
}
// ═══════════════════════════════════════════════════════════════════
// METHODS TO COMPARE (Replace with your implementations)
// ═══════════════════════════════════════════════════════════════════
private static void methodA() {
// Implementation A - e.g., String concatenation
String result = '';
for (Integer i = 0; i < 10; i++) {
result += 'item' + i + ',';
}
}
private static void methodB() {
// Implementation B - e.g., String.join()
List<String> items = new List<String>();
for (Integer i = 0; i < 10; i++) {
items.add('item' + i);
}
String result = String.join(items, ',');
}
}
// ═══════════════════════════════════════════════════════════════════════════
// DATA STRUCTURE BENCHMARK
// ═══════════════════════════════════════════════════════════════════════════
/**
* Compare List vs Set vs Map lookup performance
*
* Expected Results:
* List.contains(): O(n) - slow
* Set.contains(): O(1) - fast
* Map.containsKey(): O(1) - fast
*/
public class DataStructureBenchmark {
private static final Integer DATA_SIZE = 1000;
private static final Integer LOOKUPS = 5000;
public static void run() {
// Setup test data
List<String> testList = new List<String>();
Set<String> testSet = new Set<String>();
Map<String, Boolean> testMap = new Map<String, Boolean>();
for (Integer i = 0; i < DATA_SIZE; i++) {
String key = 'key_' + i;
testList.add(key);
testSet.add(key);
testMap.put(key, true);
}
// Random lookup keys (mix of existing and non-existing)
List<String> lookupKeys = new List<String>();
for (Integer i = 0; i < LOOKUPS; i++) {
lookupKeys.add('key_' + Math.mod(i * 7, DATA_SIZE * 2));
}
System.debug('═══════════════════════════════════════════════════════');
System.debug('DATA STRUCTURE LOOKUP BENCHMARK');
System.debug('Data Size: ' + DATA_SIZE + ' | Lookups: ' + LOOKUPS);
System.debug('═══════════════════════════════════════════════════════');
// List.contains()
Long startList = System.currentTimeMillis();
for (String key : lookupKeys) {
Boolean found = testList.contains(key);
}
Long durationList = System.currentTimeMillis() - startList;
// Set.contains()
Long startSet = System.currentTimeMillis();
for (String key : lookupKeys) {
Boolean found = testSet.contains(key);
}
Long durationSet = System.currentTimeMillis() - startSet;
// Map.containsKey()
Long startMap = System.currentTimeMillis();
for (String key : lookupKeys) {
Boolean found = testMap.containsKey(key);
}
Long durationMap = System.currentTimeMillis() - startMap;
// Results
System.debug('\nRESULTS:');
System.debug('List.contains(): ' + durationList + 'ms');
System.debug('Set.contains(): ' + durationSet + 'ms');
System.debug('Map.containsKey(): ' + durationMap + 'ms');
if (durationList > 0) {
System.debug('\nSet is ' + (durationList / Math.max(1, durationSet)) + 'x faster than List');
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// LOOP STYLE BENCHMARK
// ═══════════════════════════════════════════════════════════════════════════
/**
* Compare different loop constructs
*
* Expected Results (from documented Apex loop benchmarks):
* While loop: ~0.4s (fastest)
* Cached iterator: ~0.8s
* For loop (index): ~1.4s
* Enhanced for-each: ~2.4s
*/
public class LoopBenchmark {
private static final Integer ITERATIONS = 10000;
public static void run() {
// Create test data
List<Integer> numbers = new List<Integer>();
for (Integer i = 0; i < 1000; i++) {
numbers.add(i);
}
System.debug('═══════════════════════════════════════════════════════');
System.debug('LOOP STYLE BENCHMARK');
System.debug('Outer Iterations: ' + ITERATIONS + ' | List Size: ' + numbers.size());
System.debug('═══════════════════════════════════════════════════════');
// While loop with iterator
Long startWhile = System.currentTimeMillis();
for (Integer outer = 0; outer < ITERATIONS; outer++) {
Iterator<Integer> iter = numbers.iterator();
while (iter.hasNext()) {
Integer num = iter.next();
}
}
Long durationWhile = System.currentTimeMillis() - startWhile;
// Traditional for loop
Long startFor = System.currentTimeMillis();
for (Integer outer = 0; outer < ITERATIONS; outer++) {
for (Integer i = 0; i < numbers.size(); i++) {
Integer num = numbers[i];
}
}
Long durationFor = System.currentTimeMillis() - startFor;
// Enhanced for-each
Long startEnhanced = System.currentTimeMillis();
for (Integer outer = 0; outer < ITERATIONS; outer++) {
for (Integer num : numbers) {
// Just iterate
}
}
Long durationEnhanced = System.currentTimeMillis() - startEnhanced;
// Results
System.debug('\nRESULTS:');
System.debug('While loop: ' + durationWhile + 'ms');
System.debug('For loop: ' + durationFor + 'ms');
System.debug('Enhanced for: ' + durationEnhanced + 'ms');
System.debug('\n📊 Analysis:');
Long fastest = Math.min(durationWhile, Math.min(durationFor, durationEnhanced));
if (durationWhile == fastest) {
System.debug('🏆 While loop is fastest');
} else if (durationFor == fastest) {
System.debug('🏆 For loop is fastest');
} else {
System.debug('🏆 Enhanced for is fastest');
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// STRING BENCHMARK (demonstrates 22x improvement)
// ═══════════════════════════════════════════════════════════════════════════
/**
* String concatenation vs String.join()
*
* Expected Results (from documented Apex benchmarks):
* Concatenation: 11,767ms for 1,750 items (CPU LIMIT)
* String.join(): 539ms for 7,500 items (still running)
* Improvement: ~22x faster
*/
public class StringBenchmark {
public static void run() {
System.debug('═══════════════════════════════════════════════════════');
System.debug('STRING BUILDING BENCHMARK');
System.debug('═══════════════════════════════════════════════════════');
// Test with safe number of items
Integer itemCount = 500;
// Method 1: String concatenation (BAD)
Long startConcat = System.currentTimeMillis();
String resultConcat = '';
for (Integer i = 0; i < itemCount; i++) {
resultConcat += 'Item_' + i + '_Name\n';
}
Long durationConcat = System.currentTimeMillis() - startConcat;
// Method 2: String.join() (GOOD)
Long startJoin = System.currentTimeMillis();
List<String> items = new List<String>();
for (Integer i = 0; i < itemCount; i++) {
items.add('Item_' + i + '_Name');
}
String resultJoin = String.join(items, '\n');
Long durationJoin = System.currentTimeMillis() - startJoin;
// Results
System.debug('\nRESULTS (' + itemCount + ' items):');
System.debug('String +=: ' + durationConcat + 'ms');
System.debug('String.join(): ' + durationJoin + 'ms');
if (durationJoin > 0 && durationConcat > durationJoin) {
System.debug('\n🏆 String.join() is ' + (durationConcat / durationJoin) + 'x faster!');
}
System.debug('\n⚠️ WARNING: With larger datasets, concatenation hits CPU limit!');
System.debug(' Documented benchmark: 11,767ms for 1,750 items (concat) vs 539ms for 7,500 items (join)');
}
}
/**
* CPU TIME AND HEAP SIZE OPTIMIZATION PATTERNS
*
* CPU Limits:
* - Synchronous: 10,000 ms
* - Asynchronous: 60,000 ms
*
* Heap Limits:
* - Synchronous: 6 MB
* - Asynchronous: 12 MB
*
* This template demonstrates optimization patterns for CPU and heap limits.
*/
// ═══════════════════════════════════════════════════════════════════════════
// CPU OPTIMIZATION: ALGORITHM COMPLEXITY
// ═══════════════════════════════════════════════════════════════════════════
public class CPUOptimization {
// BEFORE: O(n²) - Nested loops are expensive
public void findDuplicatesBAD(List<Contact> contacts) {
List<Contact> duplicates = new List<Contact>();
for (Integer i = 0; i < contacts.size(); i++) {
for (Integer j = i + 1; j < contacts.size(); j++) {
if (contacts[i].Email == contacts[j].Email) {
duplicates.add(contacts[j]);
}
}
}
}
// AFTER: O(n) - Use Map for lookups
public List<Contact> findDuplicatesGOOD(List<Contact> contacts) {
Map<String, Contact> emailToContact = new Map<String, Contact>();
List<Contact> duplicates = new List<Contact>();
for (Contact c : contacts) {
if (c.Email == null) continue;
String normalizedEmail = c.Email.toLowerCase();
if (emailToContact.containsKey(normalizedEmail)) {
duplicates.add(c);
} else {
emailToContact.put(normalizedEmail, c);
}
}
return duplicates;
}
// BEFORE: Expensive string concatenation in loop
public String buildReportBAD(List<Account> accounts) {
String report = '';
for (Account acc : accounts) {
report += acc.Name + '\n'; // Creates new string each iteration!
}
return report;
}
// AFTER: Use List and join
public String buildReportGOOD(List<Account> accounts) {
List<String> lines = new List<String>();
for (Account acc : accounts) {
lines.add(acc.Name);
}
return String.join(lines, '\n');
}
}
// ═══════════════════════════════════════════════════════════════════════════
// CPU OPTIMIZATION: CACHING
// ═══════════════════════════════════════════════════════════════════════════
public class CachingPatterns {
// Cache expensive calculations
private static Map<String, Decimal> calculationCache = new Map<String, Decimal>();
public static Decimal getDiscountRate(String productCode, String region) {
String cacheKey = productCode + ':' + region;
if (calculationCache.containsKey(cacheKey)) {
return calculationCache.get(cacheKey); // Cache hit - no CPU cost
}
// Expensive calculation only happens once per unique key
Decimal rate = performExpensiveCalculation(productCode, region);
calculationCache.put(cacheKey, rate);
return rate;
}
private static Decimal performExpensiveCalculation(String productCode, String region) {
// Simulate complex calculation
return 0.15;
}
// Platform Cache for cross-transaction caching
public static Decimal getDiscountRateWithPlatformCache(String productCode, String region) {
String cacheKey = 'discount_' + productCode + '_' + region;
// Try to get from Platform Cache (Org partition)
Cache.OrgPartition orgPart = Cache.Org.getPartition('local.DiscountCache');
Decimal cachedRate = (Decimal)orgPart.get(cacheKey);
if (cachedRate != null) {
return cachedRate;
}
// Calculate and cache
Decimal rate = performExpensiveCalculation(productCode, region);
orgPart.put(cacheKey, rate, 3600); // Cache for 1 hour
return rate;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// HEAP OPTIMIZATION: LARGE DATA PROCESSING
// ═══════════════════════════════════════════════════════════════════════════
public class HeapOptimization {
// BEFORE: Loads all records into heap at once
public void processAllAccountsBAD() {
List<Account> allAccounts = [SELECT Id, Name FROM Account]; // Could be millions!
for (Account acc : allAccounts) {
// Process each account
}
}
// AFTER: SOQL FOR loop - streams records, doesn't load all into heap
public void processAllAccountsGOOD() {
for (Account acc : [SELECT Id, Name FROM Account]) {
// Each record is loaded individually
// Previous record can be garbage collected
processAccount(acc);
}
}
private void processAccount(Account acc) {
// Process single account
}
// AFTER: Batch processing for very large datasets
public void processAllAccountsBATCH() {
// Use Database.executeBatch for millions of records
Database.executeBatch(new AccountProcessorBatch(), 200);
}
}
// Batch class for large datasets
public class AccountProcessorBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator('SELECT Id, Name FROM Account');
}
public void execute(Database.BatchableContext bc, List<Account> scope) {
// Process 200 records at a time (controlled heap usage)
for (Account acc : scope) {
// Process each account
}
}
public void finish(Database.BatchableContext bc) {
// Optional: Send completion notification
}
}
// ═══════════════════════════════════════════════════════════════════════════
// HEAP OPTIMIZATION: CLEARING REFERENCES
// ═══════════════════════════════════════════════════════════════════════════
public class HeapClearingPatterns {
// Clear large collections when no longer needed
public void processWithClearing() {
// Phase 1: Query and process
List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 10000];
Map<Id, String> accountNames = new Map<Id, String>();
for (Account acc : accounts) {
accountNames.put(acc.Id, acc.Name);
}
// Clear the original list - no longer needed
accounts.clear();
accounts = null;
// Phase 2: Use the map
for (Id accId : accountNames.keySet()) {
// Process using map
}
// Clear the map when done
accountNames.clear();
accountNames = null;
}
// Use transient for Visualforce controllers
public class LargeDataController {
// transient = not stored in view state, reduces heap in page transitions
transient public List<Account> largeAccountList { get; set; }
public LargeDataController() {
largeAccountList = [SELECT Id, Name FROM Account LIMIT 5000];
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// CPU OPTIMIZATION: ASYNC PROCESSING
// ═══════════════════════════════════════════════════════════════════════════
public class AsyncProcessingPatterns {
// Move expensive operations to @future
@future
public static void processExpensiveOperation(Set<Id> recordIds) {
// Has 60 second CPU limit instead of 10 seconds
for (Id recordId : recordIds) {
// Expensive processing
}
}
// Use Queueable for chained processing
public class ExpensiveQueueable implements Queueable {
private List<Id> recordIds;
public ExpensiveQueueable(List<Id> recordIds) {
this.recordIds = recordIds;
}
public void execute(QueueableContext context) {
// Process first batch
List<Id> batch = new List<Id>();
List<Id> remaining = new List<Id>();
for (Integer i = 0; i < recordIds.size(); i++) {
if (i < 100) {
batch.add(recordIds[i]);
} else {
remaining.add(recordIds[i]);
}
}
// Process this batch
processBatch(batch);
// Chain next batch if needed
if (!remaining.isEmpty()) {
System.enqueueJob(new ExpensiveQueueable(remaining));
}
}
private void processBatch(List<Id> batch) {
// Process 100 records
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN: LIMITS MONITORING
// ═══════════════════════════════════════════════════════════════════════════
public class LimitsMonitoring {
// Check limits during execution
public void processWithLimitChecking(List<Account> accounts) {
Integer cpuWarning = 8000; // 80% of 10,000ms limit
Integer heapWarning = 4800000; // 80% of 6MB limit
for (Account acc : accounts) {
// Check CPU limit
if (Limits.getCpuTime() > cpuWarning) {
System.debug(LoggingLevel.WARN,
'CPU limit approaching: ' + Limits.getCpuTime() + 'ms');
// Consider switching to async
break;
}
// Check heap limit
if (Limits.getHeapSize() > heapWarning) {
System.debug(LoggingLevel.WARN,
'Heap limit approaching: ' + Limits.getHeapSize() + ' bytes');
break;
}
processAccount(acc);
}
}
// Debug helper: Print all limits
public static void debugLimits() {
System.debug('=== GOVERNOR LIMITS ===');
System.debug('SOQL Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML Statements: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('DML Rows: ' + Limits.getDmlRows() + '/' + Limits.getLimitDmlRows());
System.debug('CPU Time: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
System.debug('Heap Size: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());
System.debug('Callouts: ' + Limits.getCallouts() + '/' + Limits.getLimitCallouts());
}
}
/**
* DML IN LOOP FIX PATTERN
*
* Problem: DML operations inside loops consume governor limits rapidly
* Detection: DEBUG LOG shows repeated DML_BEGIN entries
* Limits:
* - 150 DML statements per transaction
* - 10,000 DML rows per transaction
*
* This template demonstrates the collection pattern to fix DML in loops.
*/
// ═══════════════════════════════════════════════════════════════════════════
// BEFORE: DML IN LOOP (PROBLEMATIC)
// ═══════════════════════════════════════════════════════════════════════════
public class ContactService_BEFORE {
// This method will fail if accounts.size() > 150
public void createContactsForAccounts(List<Account> accounts) {
for (Account acc : accounts) {
// DML inside loop - each iteration uses 1 DML statement
Contact c = new Contact(
FirstName = 'Primary',
LastName = 'Contact',
AccountId = acc.Id,
Email = acc.Name.replaceAll(' ', '') + '@example.com'
);
insert c; // DML in loop!
}
}
// Updates in loop - also problematic
public void updateAccountStatuses(List<Account> accounts, String newStatus) {
for (Account acc : accounts) {
acc.Status__c = newStatus;
update acc; // DML in loop!
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// AFTER: COLLECTION PATTERN (CORRECT)
// ═══════════════════════════════════════════════════════════════════════════
public class ContactService_AFTER {
// This method uses only 1 DML statement regardless of accounts.size()
public void createContactsForAccounts(List<Account> accounts) {
// Step 1: Build list of records to insert
List<Contact> contactsToInsert = new List<Contact>();
for (Account acc : accounts) {
Contact c = new Contact(
FirstName = 'Primary',
LastName = 'Contact',
AccountId = acc.Id,
Email = acc.Name.replaceAll(' ', '') + '@example.com'
);
contactsToInsert.add(c); // Add to list, no DML
}
// Step 2: Single DML statement AFTER the loop
if (!contactsToInsert.isEmpty()) {
insert contactsToInsert;
}
}
// Updates outside loop - correct pattern
public void updateAccountStatuses(List<Account> accounts, String newStatus) {
// Modify in loop (no DML)
for (Account acc : accounts) {
acc.Status__c = newStatus;
}
// Single DML after loop
if (!accounts.isEmpty()) {
update accounts;
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ADVANCED: MIXED INSERT/UPDATE/DELETE PATTERN
// ═══════════════════════════════════════════════════════════════════════════
public class RecordProcessor_Bulkified {
/**
* Process records with conditional insert/update/delete
* Uses collections for all DML operations
*/
public void processRecords(List<Account> accounts) {
// Collections for different DML operations
List<Contact> contactsToInsert = new List<Contact>();
List<Contact> contactsToUpdate = new List<Contact>();
List<Contact> contactsToDelete = new List<Contact>();
// Query existing contacts once
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
accountIds.add(acc.Id);
}
Map<Id, Contact> existingContacts = new Map<Id, Contact>();
for (Contact c : [
SELECT Id, AccountId, Email, IsInactive__c
FROM Contact
WHERE AccountId IN :accountIds
]) {
existingContacts.put(c.AccountId, c);
}
// Process and categorize
for (Account acc : accounts) {
Contact existing = existingContacts.get(acc.Id);
if (existing == null) {
// No contact exists - INSERT
contactsToInsert.add(new Contact(
FirstName = 'Auto',
LastName = 'Generated',
AccountId = acc.Id
));
} else if (existing.IsInactive__c) {
// Inactive contact - DELETE
contactsToDelete.add(existing);
} else {
// Active contact - UPDATE
existing.LastActivityDate__c = Date.today();
contactsToUpdate.add(existing);
}
}
// Execute DML operations (3 statements max, regardless of record count)
if (!contactsToInsert.isEmpty()) {
insert contactsToInsert;
}
if (!contactsToUpdate.isEmpty()) {
update contactsToUpdate;
}
if (!contactsToDelete.isEmpty()) {
delete contactsToDelete;
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN: UPSERT FOR INSERT-OR-UPDATE LOGIC
// ═══════════════════════════════════════════════════════════════════════════
public class UpsertPatternService {
/**
* When you need to insert if not exists, update if exists
* Use UPSERT with an External ID field
*/
public void syncExternalContacts(List<ExternalContact> externalData) {
List<Contact> contactsToUpsert = new List<Contact>();
for (ExternalContact ext : externalData) {
Contact c = new Contact(
External_Id__c = ext.id, // External ID field
FirstName = ext.firstName,
LastName = ext.lastName,
Email = ext.email
);
contactsToUpsert.add(c);
}
// Single upsert handles both insert and update
if (!contactsToUpsert.isEmpty()) {
upsert contactsToUpsert External_Id__c;
}
}
// Wrapper class for external data
public class ExternalContact {
public String id;
public String firstName;
public String lastName;
public String email;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN: PARTIAL SUCCESS HANDLING
// ═══════════════════════════════════════════════════════════════════════════
public class PartialSuccessService {
/**
* Handle partial failures gracefully
* Use Database.insert with allOrNone = false
*/
public List<Database.SaveResult> insertWithPartialSuccess(List<Contact> contacts) {
if (contacts.isEmpty()) {
return new List<Database.SaveResult>();
}
// allOrNone = false allows partial success
List<Database.SaveResult> results = Database.insert(contacts, false);
// Log failures for analysis
for (Integer i = 0; i < results.size(); i++) {
Database.SaveResult sr = results[i];
if (!sr.isSuccess()) {
for (Database.Error err : sr.getErrors()) {
System.debug(LoggingLevel.ERROR,
'Failed to insert contact: ' + contacts[i] +
' Error: ' + err.getMessage()
);
}
}
}
return results;
}
}
/**
* NULL POINTER EXCEPTION FIX PATTERNS
*
* Problem: System.NullPointerException when accessing null object properties
* Detection: DEBUG LOG shows EXCEPTION_THROWN|NullPointerException
*
* This template demonstrates null-safe coding patterns.
*/
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 1: SOQL QUERY RETURNING NO RESULTS
// ═══════════════════════════════════════════════════════════════════════════
public class QueryNullSafety {
// BEFORE: Dangerous - throws exception if no results
public String getAccountNameUNSAFE(Id accountId) {
Account acc = [SELECT Name FROM Account WHERE Id = :accountId];
return acc.Name; // NullPointerException if query returns nothing!
}
// AFTER: Safe - using List and isEmpty check
public String getAccountNameSAFE(Id accountId) {
List<Account> accounts = [SELECT Name FROM Account WHERE Id = :accountId LIMIT 1];
if (accounts.isEmpty()) {
return null; // Or throw custom exception, or return default
}
return accounts[0].Name;
}
// AFTER: Safe - using Safe Navigation Operator (API 62.0+)
public String getAccountNameSafeNav(Id accountId) {
Account acc = [SELECT Name FROM Account WHERE Id = :accountId LIMIT 1];
return acc?.Name; // Returns null instead of throwing exception
}
// AFTER: With default value
public String getAccountNameWithDefault(Id accountId, String defaultName) {
List<Account> accounts = [SELECT Name FROM Account WHERE Id = :accountId LIMIT 1];
return accounts.isEmpty() ? defaultName : accounts[0].Name;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 2: MAP ACCESS
// ═══════════════════════════════════════════════════════════════════════════
public class MapNullSafety {
// BEFORE: Dangerous - Map.get() returns null for missing keys
public void processAccountUNSAFE(Map<Id, Account> accountMap, Id targetId) {
Account acc = accountMap.get(targetId);
String name = acc.Name; // NullPointerException if targetId not in map!
}
// AFTER: Check containsKey first
public void processAccountSAFE(Map<Id, Account> accountMap, Id targetId) {
if (accountMap.containsKey(targetId)) {
Account acc = accountMap.get(targetId);
String name = acc.Name;
// Process...
}
}
// AFTER: Check for null after get
public void processAccountNullCheck(Map<Id, Account> accountMap, Id targetId) {
Account acc = accountMap.get(targetId);
if (acc != null) {
String name = acc.Name;
// Process...
}
}
// AFTER: Safe navigation (API 62.0+)
public String getAccountNameFromMap(Map<Id, Account> accountMap, Id targetId) {
return accountMap.get(targetId)?.Name;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 3: RELATIONSHIP FIELDS
// ═══════════════════════════════════════════════════════════════════════════
public class RelationshipNullSafety {
// BEFORE: Dangerous - parent record might be null
public String getOwnerEmailUNSAFE(Contact c) {
return c.Account.Owner.Email; // Multiple potential null points!
}
// AFTER: Check each level
public String getOwnerEmailSAFE(Contact c) {
if (c == null) return null;
if (c.Account == null) return null;
if (c.Account.Owner == null) return null;
return c.Account.Owner.Email;
}
// AFTER: Safe navigation chain (API 62.0+)
public String getOwnerEmailSafeNav(Contact c) {
return c?.Account?.Owner?.Email;
}
// AFTER: With default value
public String getOwnerEmailWithDefault(Contact c, String defaultEmail) {
String email = c?.Account?.Owner?.Email;
return String.isBlank(email) ? defaultEmail : email;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 4: LIST ACCESS
// ═══════════════════════════════════════════════════════════════════════════
public class ListNullSafety {
// BEFORE: Dangerous - list might be null or empty
public Contact getFirstContactUNSAFE(List<Contact> contacts) {
return contacts[0]; // IndexOutOfBoundsException if empty!
}
// AFTER: Check null and empty
public Contact getFirstContactSAFE(List<Contact> contacts) {
if (contacts == null || contacts.isEmpty()) {
return null;
}
return contacts[0];
}
// Pattern: Null-safe iteration
public void processContactsSAFE(List<Contact> contacts) {
// This is safe - for loop on null list doesn't execute
for (Contact c : contacts ?? new List<Contact>()) {
// Process contact
}
}
// Alternative: Early return
public void processContactsEarlyReturn(List<Contact> contacts) {
if (contacts == null || contacts.isEmpty()) {
return;
}
for (Contact c : contacts) {
// Process contact
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 5: STRING OPERATIONS
// ═══════════════════════════════════════════════════════════════════════════
public class StringNullSafety {
// BEFORE: Dangerous - string might be null
public Boolean containsKeywordUNSAFE(String text, String keyword) {
return text.contains(keyword); // NullPointerException if text is null!
}
// AFTER: Use String.isNotBlank() or String.isBlank()
public Boolean containsKeywordSAFE(String text, String keyword) {
if (String.isBlank(text) || String.isBlank(keyword)) {
return false;
}
return text.contains(keyword);
}
// Pattern: Null-safe string comparison
public Boolean isEqualSAFE(String a, String b) {
// Both null = equal
if (a == null && b == null) return true;
// One null = not equal
if (a == null || b == null) return false;
// Both non-null = compare
return a.equals(b);
}
// Pattern: Default value for null strings
public String getNameWithDefault(Account acc) {
return String.isBlank(acc?.Name) ? 'Unnamed Account' : acc.Name;
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN 6: TRIGGER CONTEXT
// ═══════════════════════════════════════════════════════════════════════════
public class TriggerNullSafety {
/**
* In triggers, old values might not exist (insert)
* and new values might not exist (delete)
*/
public void handleTrigger() {
// Safe: Check context first
if (Trigger.isInsert) {
// Trigger.old is null in insert context
for (Account acc : (List<Account>)Trigger.new) {
// Process new records
}
}
if (Trigger.isUpdate) {
Map<Id, Account> oldMap = (Map<Id, Account>)Trigger.oldMap;
for (Account newAcc : (List<Account>)Trigger.new) {
Account oldAcc = oldMap.get(newAcc.Id);
// Both old and new are available
if (oldAcc.Status__c != newAcc.Status__c) {
// Status changed
}
}
}
if (Trigger.isDelete) {
// Trigger.new is null in delete context
for (Account acc : (List<Account>)Trigger.old) {
// Process deleted records
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ANTI-PATTERN: OVER-DEFENSIVE CODING
// ═══════════════════════════════════════════════════════════════════════════
public class OverDefensiveCoding {
/**
* Don't check for null when it's impossible
* This adds unnecessary code and cognitive load
*/
// UNNECESSARY: Trigger.new is never null in insert/update context
public void badExample() {
if (Trigger.new != null && !Trigger.new.isEmpty()) {
for (SObject record : Trigger.new) {
// Trigger.new is guaranteed non-null in before/after insert/update
}
}
}
// CORRECT: Trust the platform guarantees
public void goodExample() {
for (Account acc : (List<Account>)Trigger.new) {
// Just use it - Trigger.new is guaranteed in this context
}
}
}
/**
* SOQL IN LOOP FIX PATTERN
*
* Problem: SOQL queries inside loops consume governor limits rapidly
* Detection: DEBUG LOG shows repeated SOQL_EXECUTE_BEGIN entries
* Limit: 100 SOQL queries per synchronous transaction
*
* This template demonstrates the bulkification pattern to fix SOQL in loops.
*/
// ═══════════════════════════════════════════════════════════════════════════
// BEFORE: SOQL IN LOOP (PROBLEMATIC)
// ═══════════════════════════════════════════════════════════════════════════
public class AccountService_BEFORE {
// This method will fail if accounts.size() > 100
public void processAccounts(List<Account> accounts) {
for (Account acc : accounts) {
// SOQL inside loop - each iteration uses 1 query
Contact primaryContact = [
SELECT Id, Email, Phone
FROM Contact
WHERE AccountId = :acc.Id
AND IsPrimary__c = true
LIMIT 1
];
if (primaryContact != null) {
acc.Primary_Contact_Email__c = primaryContact.Email;
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// AFTER: BULKIFIED PATTERN (CORRECT)
// ═══════════════════════════════════════════════════════════════════════════
public class AccountService_AFTER {
// This method uses only 1 SOQL query regardless of accounts.size()
public void processAccounts(List<Account> accounts) {
// Step 1: Collect all Account IDs
Set<Id> accountIds = new Set<Id>();
for (Account acc : accounts) {
accountIds.add(acc.Id);
}
// Step 2: Single SOQL query BEFORE the loop
Map<Id, Contact> primaryContactsByAccountId = new Map<Id, Contact>();
for (Contact c : [
SELECT Id, Email, Phone, AccountId
FROM Contact
WHERE AccountId IN :accountIds
AND IsPrimary__c = true
]) {
primaryContactsByAccountId.put(c.AccountId, c);
}
// Step 3: Access from Map inside loop (no SOQL)
for (Account acc : accounts) {
Contact primaryContact = primaryContactsByAccountId.get(acc.Id);
if (primaryContact != null) {
acc.Primary_Contact_Email__c = primaryContact.Email;
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ADVANCED: NESTED RELATIONSHIP PATTERN
// ═══════════════════════════════════════════════════════════════════════════
public class OrderService_Bulkified {
// Complex scenario: Orders → Accounts → Contacts
public void processOrders(List<Order> orders) {
// Collect all Account IDs from Orders
Set<Id> accountIds = new Set<Id>();
for (Order o : orders) {
if (o.AccountId != null) {
accountIds.add(o.AccountId);
}
}
// Query Accounts with related Contacts in ONE query (relationship query)
Map<Id, Account> accountsWithContacts = new Map<Id, Account>([
SELECT Id, Name,
(SELECT Id, Email FROM Contacts WHERE IsPrimary__c = true LIMIT 1)
FROM Account
WHERE Id IN :accountIds
]);
// Process orders using the pre-queried data
for (Order o : orders) {
Account acc = accountsWithContacts.get(o.AccountId);
if (acc != null && !acc.Contacts.isEmpty()) {
o.Notification_Email__c = acc.Contacts[0].Email;
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// PATTERN: MAP FACTORY FOR REUSABLE QUERIES
// ═══════════════════════════════════════════════════════════════════════════
public class ContactQueryService {
/**
* Factory method to get primary contacts by Account ID
* Reusable across multiple services
*/
public static Map<Id, Contact> getPrimaryContactsByAccountId(Set<Id> accountIds) {
Map<Id, Contact> result = new Map<Id, Contact>();
if (accountIds.isEmpty()) {
return result;
}
for (Contact c : [
SELECT Id, FirstName, LastName, Email, Phone, AccountId
FROM Contact
WHERE AccountId IN :accountIds
AND IsPrimary__c = true
]) {
result.put(c.AccountId, c);
}
return result;
}
/**
* Get all contacts grouped by Account ID
* Returns Map<AccountId, List<Contact>>
*/
public static Map<Id, List<Contact>> getContactsGroupedByAccount(Set<Id> accountIds) {
Map<Id, List<Contact>> result = new Map<Id, List<Contact>>();
// Initialize empty lists for all accounts
for (Id accId : accountIds) {
result.put(accId, new List<Contact>());
}
for (Contact c : [
SELECT Id, FirstName, LastName, Email, Phone, AccountId
FROM Contact
WHERE AccountId IN :accountIds
ORDER BY CreatedDate DESC
]) {
result.get(c.AccountId).add(c);
}
return result;
}
}
platform-apex-logs-debug
Salesforce debugging and troubleshooting skill with log analysis, governor limit detection, and agentic fix suggestions. Identify performance bottlenecks and automatically suggest fixes.
Features
- Log Analysis: Parse and analyze Apex debug logs
- Governor Limit Detection: SOQL, DML, CPU, and heap limit monitoring
- Performance Analysis: Find slow queries and expensive operations
- Stack Trace Interpretation: Parse exceptions and identify root causes
- Agentic Fix Suggestions: Automatically suggest code fixes
- 100-Point Scoring: Issue severity classification
Quick Start
1. Invoke the skill
Skill: platform-apex-logs-debug
Request: "Analyze debug logs for AccountTrigger performance issues in org dev"2. Common operations
| Operation | Example Request |
|---|---|
| Analyze log | "Analyze the latest debug log for errors" |
| List logs | "Show recent debug logs for org dev" |
| Tail logs | "Tail logs for user admin@company.com" |
| Find limits | "Find governor limit issues in the log" |
| Query plan | "Analyze query plan for Account SOQL" |
Log Commands
# List recent logs
sf apex list log --target-org [alias] --json
# Get specific log
sf apex get log --log-id 07Lxx0000000000 --target-org [alias]
# Tail logs real-time
sf apex tail log --target-org [alias] --colorScoring System (100 Points)
| Category | Points | Focus |
|---|---|---|
| Governor Limits | 25 | SOQL, DML, CPU, Heap analysis |
| Performance | 25 | Query optimization, N+1 detection |
| Error Analysis | 20 | Exception handling, stack traces |
| Root Cause | 20 | Issue identification, fix suggestions |
| Documentation | 10 | Clear explanations, actionable fixes |
Cross-Skill Integration
| Related Skill | When to Use |
|---|---|
| platform-apex-generate | Fix identified Apex issues |
| platform-soql-query | Optimize slow SOQL queries |
| platform-apex-test-run | Re-run tests after fixes |
Documentation
- Benchmarking Guide
Requirements
- sf CLI v2
- Target Salesforce org
- Debug logs enabled for target user
Debug Analysis Playbook
Use this playbook when platform-apex-logs-debug is active and you need the expanded workflow.
1. Gather context
Collect:
- org alias
- failing transaction or test context
- approximate time window
- relevant user / record / request identifiers
2. Retrieve logs
Preferred commands:
sf apex list log --target-org <alias> --json
sf apex get log --log-id <id> --target-org <alias>
sf apex tail log --target-org <alias> --colorSee cli-commands.md for more options.
3. Analyze in this order
1. transaction entry point 2. exceptions and fatal errors 3. governor limits 4. SOQL / DML repetition patterns 5. CPU / heap hotspots 6. callout timing and external failures
4. Classify severity
- Critical — runtime failure, hard limit, data corruption risk
- Warning — near-limit, non-selective query, slow path
- Info — optimization opportunity, cleanup item, observability gap
5. Propose fixes
Prefer fixes that are:
- root-cause oriented
- bulk-safe
- testable
- deployable in one clean change set
6. Loop with adjacent skills
- use
sf-apexfor code fixes - use
platform-apex-test-runto reproduce and verify - use
platform-metadata-deployto deploy fixes - use
platform-data-managewhen the issue depends on missing or malformed test data
<!-- Parent: platform-apex-logs-debug/SKILL.md -->
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
---
Related Resources
- assets/benchmarking-template.cls - Ready-to-use benchmark template
- assets/cpu-heap-optimization.cls - Optimization patterns
- Apex Log Analyzer - Visual performance analysis
<!-- Parent: platform-apex-logs-debug/SKILL.md -->
Salesforce CLI Debug Commands Reference
Quick Reference
| Task | Command |
|---|---|
| List recent logs | sf apex list log --target-org my-org |
| Get specific log | sf apex get log --log-id 07Lxx0000000000 |
| Stream real-time | sf apex tail log --target-org my-org |
| Delete log | sf data delete record --sobject ApexLog --record-id <id> --target-org my-org |
---
Log Retrieval
List Available Logs
# List all logs for current user
sf apex list log --target-org my-org
# JSON output for parsing
sf apex list log --target-org my-org --jsonOutput Fields:
Id- Log ID for retrievalLogUser.Name- Who generated the logOperation- What triggered the logStatus- Success/FailureLogLength- Size in bytesStartTime- When it was generated
Get Specific Log
# Download log by ID
sf apex get log \
--log-id 07Lxx0000000000AAA \
--target-org my-org
# Save to file
sf apex get log \
--log-id 07Lxx0000000000AAA \
--target-org my-org \
--output-dir ./logs
# Get most recent log
sf apex list log --target-org my-org --json | \
jq -r '.result[0].Id' | \
xargs -I {} sf apex get log --log-id {} --target-org my-orgStream Logs Real-Time
# Tail logs with color highlighting
sf apex tail log --target-org my-org --color
# Tail with color highlighting
sf apex tail log \
--target-org my-org \
--color
# Note: Debug levels are configured via TraceFlag records in Setup, not CLI flags
# Tail with skip flag (don't show historical logs)
sf apex tail log --target-org my-org --skip-trace-flag---
Log Management
Delete Logs
Note:sf apex log deletedoes not exist in sf CLI v2. Usesf data delete recordinstead.
# Delete specific log
sf data delete record \
--sobject ApexLog \
--record-id 07Lxx0000000000AAA \
--target-org my-org---
Debug Level Configuration
Using Trace Flags
Trace flags control what gets logged and for which users.
# Create trace flag for specific user
sf data create record \
--sobject TraceFlag \
--values "TracedEntityId='005xx0000001234' LogType='USER_DEBUG' DebugLevelId='7dlxx0000000000' StartDate='2025-01-01T00:00:00Z' ExpirationDate='2025-01-02T00:00:00Z'" \
--target-org my-org
# Query existing trace flags
sf data query \
--query "SELECT Id, TracedEntityId, DebugLevel.MasterLabel, ExpirationDate FROM TraceFlag" \
--target-org my-org
# Delete trace flag
sf data delete record \
--sobject TraceFlag \
--record-id 7tfxx0000000000AAA \
--target-org my-orgDebug Levels
# List available debug levels
sf data query \
--query "SELECT Id, MasterLabel, ApexCode, ApexProfiling, Callout, Database, System, Workflow FROM DebugLevel" \
--target-org my-org
# Create custom debug level
sf data create record \
--sobject DebugLevel \
--values "MasterLabel='PerformanceDebug' DeveloperName='PerformanceDebug' ApexCode='FINE' ApexProfiling='FINEST' Database='FINE' System='DEBUG'" \
--target-org my-org---
Advanced Usage
Execute Anonymous with Logging
# Run anonymous Apex and capture log
echo "System.debug('Test'); Account a = [SELECT Id FROM Account LIMIT 1];" | \
sf apex run --target-org my-org
# Run from file
sf apex run \
--file ./scripts/debug-script.apex \
--target-org my-org
# Get the log from that execution
sf apex list log --target-org my-org --json | \
jq -r '.result[0].Id' | \
xargs -I {} sf apex get log --log-id {} --target-org my-orgQuery Plan Analysis
# Analyze query plan using Tooling API
sf data query \
--query "SELECT Id FROM Account WHERE Name = 'Test'" \
--target-org my-org \
--use-tooling-api
# Note: --explain does not exist. Use REST API for query plans:
# GET /services/data/v66.0/query/?explain=SELECT+Id+FROM+Account+WHERE+Name='Test'Log Analysis with grep
# Find SOQL queries in log
sf apex get log --log-id 07Lxx0000000000 --target-org my-org | \
rg "SOQL_EXECUTE"
# Count SOQL queries
sf apex get log --log-id 07Lxx0000000000 --target-org my-org | \
rg -c "SOQL_EXECUTE_BEGIN"
# Find exceptions
sf apex get log --log-id 07Lxx0000000000 --target-org my-org | \
rg "EXCEPTION_THROWN|FATAL_ERROR"
# Find limit usage
sf apex get log --log-id 07Lxx0000000000 --target-org my-org | \
rg "LIMIT_USAGE"
# Find slow operations (method timing)
sf apex get log --log-id 07Lxx0000000000 --target-org my-org | \
rg "METHOD_EXIT.*\|([0-9]{4,})\|"---
Automation Scripts
Save and Analyze Latest Log
#!/bin/bash
# save-latest-log.sh
ORG_ALIAS=${1:-"my-org"}
OUTPUT_DIR=${2:-"./logs"}
mkdir -p $OUTPUT_DIR
# Get latest log ID
LOG_ID=$(sf apex list log --target-org $ORG_ALIAS --json | jq -r '.result[0].Id')
if [ "$LOG_ID" == "null" ]; then
echo "No logs found"
exit 1
fi
# Save log
FILENAME="$OUTPUT_DIR/$(date +%Y%m%d_%H%M%S)_$LOG_ID.log"
sf apex get log --log-id $LOG_ID --target-org $ORG_ALIAS > $FILENAME
echo "Log saved to: $FILENAME"
# Quick analysis
echo ""
echo "=== QUICK ANALYSIS ==="
echo "SOQL Queries: $(rg -c 'SOQL_EXECUTE_BEGIN' $FILENAME || echo 0)"
echo "DML Statements: $(rg -c 'DML_BEGIN' $FILENAME || echo 0)"
echo "Exceptions: $(rg -c 'EXCEPTION_THROWN|FATAL_ERROR' $FILENAME || echo 0)"Monitor for Errors
#!/bin/bash
# monitor-errors.sh
ORG_ALIAS=${1:-"my-org"}
echo "Monitoring $ORG_ALIAS for errors..."
echo "Press Ctrl+C to stop"
sf apex tail log --target-org $ORG_ALIAS --color 2>&1 | \
while read line; do
if echo "$line" | rg -q "EXCEPTION|FATAL_ERROR|LimitException"; then
echo "🔴 ERROR DETECTED: $line"
# Optional: Send alert
# osascript -e 'display notification "Error in Salesforce" with title "platform-apex-logs-debug"'
fi
doneBulk Log Cleanup
#!/bin/bash
# cleanup-logs.sh
ORG_ALIAS=${1:-"my-org"}
DAYS_OLD=${2:-7}
echo "Deleting logs older than $DAYS_OLD days from $ORG_ALIAS..."
# Get old log IDs
OLD_LOGS=$(sf data query \
--query "SELECT Id FROM ApexLog WHERE StartTime < LAST_N_DAYS:$DAYS_OLD" \
--target-org $ORG_ALIAS \
--json | jq -r '.result.records[].Id')
COUNT=0
for LOG_ID in $OLD_LOGS; do
sf data delete record --sobject ApexLog --record-id $LOG_ID --target-org $ORG_ALIAS --json > /dev/null
((COUNT++))
done
echo "Deleted $COUNT logs"---
Troubleshooting
No Logs Appearing
1. Check trace flags exist:
sf data query \
--query "SELECT Id, TracedEntityId, ExpirationDate FROM TraceFlag WHERE ExpirationDate > TODAY" \
--target-org my-org2. Check log retention:
sf data query \
--query "SELECT Id, StartTime FROM ApexLog ORDER BY StartTime DESC LIMIT 5" \
--target-org my-org3. Verify user has API access:
- User must have "API Enabled" permission
- User must have "Author Apex" for trace flags
Log Too Large
Logs over 2MB are truncated. Solutions:
1. Reduce debug level:
ApexCode: DEBUG → INFO
ApexProfiling: FINEST → FINE2. Focus on specific operation:
- Create trace flag just before the operation
- Delete after capturing
3. Use targeted logging:
- Add
System.debug()only where needed - Use
LoggingLevel.ERRORfor critical info
Logs Not Persisting
Default log retention is 24 hours. To keep logs longer:
# Export log to file immediately
sf apex get log --log-id 07Lxx0000000000 --target-org my-org > ./saved-log.txt---
Integration with platform-apex-logs-debug Skill
The platform-apex-logs-debug skill automatically:
1. Fetches logs when you run sf apex get log or sf apex tail log 2. Parses content for SOQL in loops, DML in loops, exceptions 3. Displays analysis with governor limit usage 4. Suggests fixes using sf-apex skill integration
Example workflow:
# Run a test that generates a log
sf apex run test --class-names MyTestClass --target-org my-org
# Get the log (platform-apex-logs-debug hook auto-analyzes)
sf apex list log --target-org my-org --json | \
jq -r '.result[0].Id' | \
xargs -I {} sf apex get log --log-id {} --target-org my-orgThe hook will output analysis like:
============================================================
🔍 DEBUG LOG ANALYSIS
============================================================
🔴 CRITICAL ISSUES
------------------------------------------------------------
• SOQL in loop detected: 50 queries executed inside loops
• CPU limit critical: 9500/10000ms (95.0%)
📊 GOVERNOR LIMIT USAGE
------------------------------------------------------------
✅ SOQL Queries: 50/100 (50.0%)
✅ DML Statements: 25/150 (16.7%)
🔴 CPU Time (ms): 9500/10000 (95.0%)
🤖 AGENTIC FIX RECOMMENDATIONS
============================================================
For SOQL in loop:
1. Move query BEFORE the loop
2. Store results in Map<Id, SObject>
3. Access from Map inside loopCommon Debug Log Issues
SOQL in loop
Signals
- repeating
SOQL_EXECUTE_BEGIN - query appears inside repeated method path
Fix pattern
- query once outside the loop
- use
Map<Id, SObject>or grouped collections
DML in loop
Signals
- repeated
DML_BEGIN - high DML statement count for small transactions
Fix pattern
- collect changes
- do one bulk DML operation
Non-selective query
Signals
- high rows scanned
- slow query timing
- table-scan indicators
Fix pattern
- add indexed filters
- reduce scope
- use query-plan guidance
CPU pressure
Signals
- CPU usage trending toward sync limit
- repeated expensive helper methods
- nested loops / repeated string work
Fix pattern
- reduce algorithmic complexity
- cache repeated work
- move heavy processing async where appropriate
Heap pressure
Signals
- large collection allocations
- heap usage approaching sync limit
Fix pattern
- use SOQL for-loops
- reduce in-memory object size
- clear collections when done
Null pointer / unhandled exceptions
Signals
EXCEPTION_THROWNFATAL_ERROR- clear stack trace with line numbers
Fix pattern
- guard null values
- make assumptions explicit
- improve result handling for empty query results
<!-- Parent: platform-apex-logs-debug/SKILL.md -->
Salesforce Debug Log Reference
Log Structure
Debug logs follow a consistent format:
TIMESTAMP|EVENT_IDENTIFIER|[PARAMS]|DETAILSExample:
14:32:54.123 (123456789)|SOQL_EXECUTE_BEGIN|[45]|SELECT Id FROM Account---
Event Categories
Execution Events
| Event | Description | Analysis Notes |
|---|---|---|
EXECUTION_STARTED | Transaction begins | Identifies transaction type |
EXECUTION_FINISHED | Transaction ends | Total execution time |
CODE_UNIT_STARTED | Method/trigger entry | Call stack tracing |
CODE_UNIT_FINISHED | Method/trigger exit | Method duration |
SOQL Events
| Event | Description | Key Fields |
|---|---|---|
SOQL_EXECUTE_BEGIN | Query starts | Line number, Query text |
SOQL_EXECUTE_END | Query ends | Rows returned |
Analysis Pattern:
|SOQL_EXECUTE_BEGIN|[45]|SELECT Id, Name FROM Account WHERE...
|SOQL_EXECUTE_END|[3 rows]- Line 45 in source code
- Query returned 3 rows
Warning Signs:
- Same query appearing multiple times → SOQL in loop
[100000 rows]→ Non-selective query- Query without WHERE clause → Full table scan
DML Events
| Event | Description | Key Fields |
|---|---|---|
DML_BEGIN | DML starts | Line number, Operation type, Object |
DML_END | DML ends | Rows affected |
Analysis Pattern:
|DML_BEGIN|[78]|Op:Insert|Type:Contact|
|DML_END|[200 rows]- Line 78: INSERT operation
- 200 Contact records inserted
Warning Signs:
- Same DML operation appearing multiple times → DML in loop
- DML after each SOQL → Likely unbulkified code
Limit Events
| Event | Description | Format |
|---|---|---|
LIMIT_USAGE | Current limit usage | `LIMIT_NAME |
LIMIT_USAGE_FOR_NS | Per-namespace limits | `LIMIT_NAME |
CUMULATIVE_LIMIT_USAGE | End of transaction | Summary of all limits |
Example:
|LIMIT_USAGE|SOQL_QUERIES|25|100
|LIMIT_USAGE|DML_STATEMENTS|45|150
|LIMIT_USAGE|CPU_TIME|3500|10000
|LIMIT_USAGE|HEAP_SIZE|2500000|6000000Exception Events
| Event | Description | Format |
|---|---|---|
EXCEPTION_THROWN | Exception occurs | `[line] |
FATAL_ERROR | Unhandled exception | Full stack trace |
Analysis Pattern:
|EXCEPTION_THROWN|[67]|System.NullPointerException|Attempt to de-reference a null object
|FATAL_ERROR|System.NullPointerException: Attempt to de-reference a null object
Class.ContactService.processContacts: line 67, column 1
Class.AccountTriggerHandler.afterUpdate: line 34, column 1
Trigger.AccountTrigger: line 5, column 1Method Events
| Event | Description | Use Case |
|---|---|---|
METHOD_ENTRY | Method called | Call hierarchy |
METHOD_EXIT | Method returns | Method duration |
CONSTRUCTOR_ENTRY | Constructor called | Object creation |
CONSTRUCTOR_EXIT | Constructor returns |
Loop Events
| Event | Description | Important For |
|---|---|---|
LOOP_BEGIN | Loop starts | SOQL/DML in loop detection |
LOOP_END | Loop ends | |
ITERATION_BEGIN | Loop iteration | Iteration count |
ITERATION_END | Iteration ends |
Detection Pattern for SOQL in Loop:
|LOOP_BEGIN|
|ITERATION_BEGIN|
|SOQL_EXECUTE_BEGIN|[45]|SELECT...
|SOQL_EXECUTE_END|[1 rows]
|ITERATION_END|
|ITERATION_BEGIN|
|SOQL_EXECUTE_BEGIN|[45]|SELECT... ← Same query repeating!
|SOQL_EXECUTE_END|[1 rows]
|ITERATION_END|
|LOOP_END|Callout Events
| Event | Description | Key Fields |
|---|---|---|
CALLOUT_EXTERNAL_ENTRY | HTTP callout starts | Endpoint URL |
CALLOUT_EXTERNAL_EXIT | HTTP callout ends | Status code, Duration |
Example:
|CALLOUT_EXTERNAL_ENTRY|[89]|https://api.example.com/endpoint
|CALLOUT_EXTERNAL_EXIT|[200]|[1500ms]Heap Events
| Event | Description | Importance |
|---|---|---|
HEAP_ALLOCATE | Heap allocation | Track large allocations |
HEAP_DEALLOCATE | Heap freed | Garbage collection |
---
Log Levels
Categories and Levels
| Category | What It Controls |
|---|---|
| Database | SOQL, SOSL, DML |
| Workflow | Workflow rules, Process Builder |
| Validation | Validation rules |
| Callout | HTTP callouts |
| Apex Code | Apex execution |
| Apex Profiling | Method timing |
| Visualforce | VF page execution |
| System | System operations |
Level Values
| Level | Amount of Detail | Use Case |
|---|---|---|
| NONE | Nothing | Disable category |
| ERROR | Errors only | Production monitoring |
| WARN | Warnings + errors | |
| INFO | General info | Default |
| DEBUG | Detailed debug | Development |
| FINE | Very detailed | Deep debugging |
| FINER | Method entry/exit | Performance analysis |
| FINEST | Everything | Complete trace |
Recommended Debug Level Settings
For Performance Issues:
Apex Code: FINE
Apex Profiling: FINEST
Database: FINE
System: DEBUGFor Exception Debugging:
Apex Code: DEBUG
Apex Profiling: FINE
Database: INFO
System: DEBUGFor Callout Issues:
Apex Code: DEBUG
Callout: FINEST
System: DEBUG---
Governor Limits Reference
Synchronous Limits
| Limit | Value | Log Event |
|---|---|---|
| SOQL Queries | 100 | SOQL_QUERIES |
| SOQL Rows | 50,000 | SOQL_ROWS |
| DML Statements | 150 | DML_STATEMENTS |
| DML Rows | 10,000 | DML_ROWS |
| CPU Time | 10,000 ms | CPU_TIME |
| Heap Size | 6 MB | HEAP_SIZE |
| Callouts | 100 | CALLOUTS |
| Future Calls | 50 | FUTURE_CALLS |
Asynchronous Limits
| Limit | Value | Applies To |
|---|---|---|
| CPU Time | 60,000 ms | @future, Batch, Queueable |
| Heap Size | 12 MB | @future, Batch, Queueable |
Warning Thresholds
| Limit | Warning (80%) | Critical (95%) |
|---|---|---|
| SOQL Queries | 80 | 95 |
| DML Statements | 120 | 143 |
| CPU Time | 8,000 ms | 9,500 ms |
| Heap Size | 4.8 MB | 5.7 MB |
---
Common Log Patterns
Pattern 1: SOQL in Loop
|LOOP_BEGIN|
|ITERATION_BEGIN|
|SOQL_EXECUTE_BEGIN|[45]|SELECT Id FROM Contact WHERE AccountId = '001xxx'
|SOQL_EXECUTE_END|[1 rows]
|ITERATION_END|
|ITERATION_BEGIN|
|SOQL_EXECUTE_BEGIN|[45]|SELECT Id FROM Contact WHERE AccountId = '001yyy'
|SOQL_EXECUTE_END|[1 rows]
|ITERATION_END|
... (repeats 100 times)
|LIMIT_USAGE|SOQL_QUERIES|100|100 ← LIMIT HIT!
|FATAL_ERROR|System.LimitException: Too many SOQL queries: 101Pattern 2: DML in Loop
|LOOP_BEGIN|
|ITERATION_BEGIN|
|DML_BEGIN|[78]|Op:Insert|Type:Contact|
|DML_END|[1 rows]
|ITERATION_END|
... (repeats 150 times)
|LIMIT_USAGE|DML_STATEMENTS|150|150 ← LIMIT HIT!
|FATAL_ERROR|System.LimitException: Too many DML statements: 151Pattern 3: Non-Selective Query
|SOQL_EXECUTE_BEGIN|[23]|SELECT Id FROM Lead WHERE Status = 'Open'
|SOQL_EXECUTE_END|[250000 rows] ← Large result set!Pattern 4: CPU Limit Approaching
|CUMULATIVE_LIMIT_USAGE|
|CPU_TIME|9500|10000 ← 95% used!Pattern 5: Null Pointer Exception
|SOQL_EXECUTE_BEGIN|[45]|SELECT Id FROM Account WHERE Id = '001xxx'
|SOQL_EXECUTE_END|[0 rows] ← No results!
|METHOD_EXIT|getAccount|
|EXCEPTION_THROWN|[47]|System.NullPointerException|Attempt to de-reference a null object---
Log Analysis Checklist
Quick Scan
1. Search for `FATAL_ERROR` - Find the exception 2. Search for `LIMIT_USAGE` - Check governor limits 3. Search for `SOQL_EXECUTE_BEGIN` - Count queries 4. Search for `DML_BEGIN` - Count DML operations 5. Search for `LOOP_BEGIN` - Check for operations in loops
Deep Analysis
1. Trace the call stack - Use CODE_UNIT_STARTED events 2. Find the hotspot - Use Apex Profiling: FINEST for method timing 3. Identify large queries - Look for [N rows] in SOQL_EXECUTE_END 4. Check callout timing - Look for slow CALLOUT_EXTERNAL_EXIT 5. Monitor heap growth - Track HEAP_ALLOCATE events
---
Related Commands
| Command | Purpose |
|---|---|
sf apex list log | List available logs |
sf apex get log --log-id XXX | Download specific log |
sf apex tail log | Stream logs real-time |
sf data delete record --sobject ApexLog --record-id <id> | Delete individual log record |
See cli-commands.md for detailed command reference.
<!-- Parent: platform-apex-logs-debug/SKILL.md -->
Log Analysis Tools
This guide covers tools for analyzing Salesforce debug logs, with a focus on performance profiling and bottleneck identification.
---
Recommended: Apex Log Analyzer (VS Code)
The Apex Log Analyzer is a free VS Code extension that provides visual analysis of Apex debug logs.
Installation
1. Open VS Code
2. Go to Extensions (Ctrl+Shift+X / Cmd+Shift+X)
3. Search "Apex Log Analyzer"
4. Install "Apex Log Analyzer" by FinancialForceOr install via command line:
code --install-extension FinancialForce.lanaKey Features
| Feature | Description | Use Case |
|---|---|---|
| Flame Charts | Visual execution timeline | Find slow methods at a glance |
| Call Tree | Hierarchical method calls | Trace execution paths |
| Database Analysis | SOQL/DML highlighting | Find query hotspots |
| Timeline View | Execution over time | See parallel operations |
| Limit Summary | Governor limit usage | Quick health check |
How to Use
1. Get a debug log:
sf apex get log --log-id 07Lxx0000000000 --target-org my-sandbox -o debug.log2. Open in VS Code:
- Open the
.logfile - Click "Analyze Log" button in the editor toolbar
- Or use Command Palette: "Apex Log: Analyze Log"
3. Navigate the visualization:
- Flame Chart: Wider bars = more time spent
- Click methods to see details
- Hover for exact timing
- Filter to focus on specific operations
Reading Flame Charts
┌─────────────────────────────────────────────────────────────┐
│ FLAME CHART INTERPRETATION │
├─────────────────────────────────────────────────────────────┤
│ │
│ ████████████████████████████████████ AccountTrigger │
│ ██████████████████ AccountService.processAccounts │
│ ██████████ ← BOTTLENECK: Wide bar = slow operation │
│ ████ SOQL query │
│ ████████████ Another slow method │
│ │
│ Time flows left → right │
│ Width = execution time │
│ Stack depth = call hierarchy │
│ │
└─────────────────────────────────────────────────────────────┘Bottleneck Indicators:
- Wide bars at lower levels (deep in call stack)
- Multiple identical bars (repeated operations)
- SOQL/DML bars inside loop patterns
---
SF CLI Debug Commands
List Available Logs
# List recent logs
sf apex list log --target-org my-sandbox --json
# Output includes:
# - Log ID
# - Start time
# - Operation type
# - User
# - SizeDownload Logs
# Download specific log
sf apex get log --log-id 07Lxx0000000000 --target-org my-sandbox
# Save to file
sf apex get log --log-id 07Lxx0000000000 --target-org my-sandbox > debug.log
# Download with number
sf apex get log --number 1 --target-org my-sandbox # Most recentReal-Time Log Streaming
# Tail logs in real-time (like `tail -f`)
sf apex tail log --target-org my-sandbox
# With color highlighting
sf apex tail log --target-org my-sandbox --color
# Use an existing debug level when setting the trace flag
sf apex tail log --target-org my-sandbox --debug-level MyDebugLevel
# Skip trace flag setup when a trace flag is already configured
sf apex tail log --target-org my-sandbox --skip-trace-flagSet Debug Level
# Create trace flag via SFDX
sf data create record \
--sobject TraceFlag \
--values "TracedEntityId='005xx...' LogType='USER_DEBUG' StartDate='2024-01-01T00:00:00' ExpirationDate='2024-01-02T00:00:00'" \
--target-org my-sandbox---
Manual Log Analysis (grep/ripgrep)
When you need quick analysis without visual tools:
Find All SOQL Queries
# Count SOQL queries
rg "SOQL_EXECUTE_BEGIN" debug.log | wc -l
# See query text
rg "SOQL_EXECUTE_BEGIN" debug.log
# Find SOQL in loops (query appears multiple times on same line)
rg "SOQL_EXECUTE_BEGIN" debug.log | sort | uniq -c | sort -rn | head -10Find Large Result Sets
# Find queries returning many rows
rg "SOQL_EXECUTE_END.*\[\d{4,} rows\]" debug.logCheck Governor Limits
# Find limit snapshots
rg "LIMIT_USAGE" debug.log | tail -20
# Find approaching limits (>80%)
rg "CPU_TIME" debug.log | tail -5
rg "HEAP_SIZE" debug.log | tail -5Find Exceptions
# Find all exceptions
rg "EXCEPTION_THROWN|FATAL_ERROR" debug.log
# Get stack traces
rg -A 10 "FATAL_ERROR" debug.logFind Slow Operations
# Extract all timing info
rg "\[\d+\]ms" debug.log | sort -t'[' -k2 -rn | head -20---
Developer Console Analysis
For quick checks directly in Salesforce:
Query Plan Tool
1. Open Developer Console 2. Query Editor tab 3. Click "Query Plan" checkbox 4. Run your query 5. Review selectivity and cost
Log Inspector
1. Open Developer Console 2. Debug → Open Execute Anonymous Window 3. Run code to generate log 4. Select log in "Logs" tab 5. Debug → View Log Panels
Useful Panels:
- Execution Overview: Summary of operations
- Timeline: Visual execution flow
- Stack Tree: Call hierarchy
- Database: SOQL/DML details
---
Quick Reference: What to Look For
| Problem | What to Search | Pattern |
|---|---|---|
| SOQL in Loop | SOQL_EXECUTE_BEGIN | Same query repeated many times |
| DML in Loop | DML_BEGIN | Same DML repeated many times |
| Slow Query | SOQL_EXECUTE_END | Large row count |
| CPU Issue | CPU_TIME | Approaching 10,000ms |
| Heap Issue | HEAP_SIZE | Approaching 6,000,000 |
| Exception | EXCEPTION_THROWN | Stack trace |
---
Comparison: Analysis Tools
| Tool | Pros | Cons | Best For |
|---|---|---|---|
| Apex Log Analyzer | Visual, free, flame charts | Requires VS Code | Deep performance analysis |
| Developer Console | Built-in, no install | Limited visualization | Quick checks |
| SF CLI + grep | Scriptable, fast | No visualization | CI/CD, automation |
| Salesforce Inspector | Browser-based | Limited log analysis | Quick org exploration |
---
Related Resources
- debug-log-reference.md - Log event types
- benchmarking-guide.md - Performance testing
- cli-commands.md - SF CLI reference
Debug Analysis Scoring Rubric
100-point rubric
| Category | Points | What good looks like |
|---|---|---|
| Root-cause accuracy | 25 | Finds the actual cause, not just symptoms |
| Fix quality | 25 | Recommends a fix that directly addresses the cause |
| Performance impact | 20 | Improves limits / efficiency without introducing regressions |
| Completeness | 15 | Captures related issues and secondary risks |
| Clarity | 15 | Gives an explanation the user can act on quickly |
Thresholds
| Score | Rating | Meaning |
|---|---|---|
| 90–100 | Excellent | Expert analysis with strong fixes |
| 80–89 | Good | Reliable analysis with minor gaps |
| 70–79 | Acceptable | Usable, but may miss secondary issues |
| 60–69 | Weak | Partial diagnosis only |
| < 60 | Incomplete | Needs more investigation |
Related skills
FAQ
Is Platform Apex Logs Debug safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.