Trace Parser & Optimization
Use Trace Parser to find bottlenecks, optimise data entity imports, design effective indexes, and tune batch processes with the Async and Sandbox frameworks.
Why trace before you optimise?
Think of a doctor.
A good doctor doesnβt prescribe medicine based on a guess β they run tests first. Trace Parser is your diagnostic tool. It records everything F&O does during an operation (which methods run, which SQL queries fire, how long each takes) and shows you exactly where the time is spent.
Without tracing, youβre guessing. With tracing, you can see: β80% of this batch jobβs time is spent in one SQL query that scans 2 million rows because thereβs no index.β Now you know exactly what to fix.
Trace Parser: capture and analyse
Capturing a trace
| Step | Action |
|---|---|
| 1 | Navigate to the operation you want to trace (e.g. a slow form or batch job) |
| 2 | Open System Administration β Setup β Trace β enable tracing |
| 3 | Set trace options: SQL traces (essential), X++ method traces (for code analysis) |
| 4 | Perform the slow operation |
| 5 | Stop tracing |
| 6 | Download the trace file (.etl) |
| 7 | Open in Trace Parser desktop tool for analysis |
What the trace shows you
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Trace Parser β
β β
β ββ Call Tree ββββββββββββββββββββββββββββββββββββββ β
β β processReport() [Total: 45.2s] β β
β β ββ loadData() [38.1s] β HOTSPOT β β
β β β ββ SELECT FROM InventTrans [32.4s] β BAD SQL β β
β β β ββ calculateCost() [5.7s] β β
β β ββ formatOutput() [7.1s] β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββ SQL Statements βββββββββββββββββββββββββββββββββ β
β β #1: SELECT ... FROM InventTrans WHERE ... β β
β β Duration: 32.4s | Rows: 2,100,000 β β
β β Table Scan (no index used) β ROOT CAUSE β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key metrics to examine
| Metric | What it tells you | Action threshold |
|---|---|---|
| Method duration | How long each X++ method takes | > 1s for interactive, > 10s for batch steps |
| SQL duration | Time spent in individual SQL statements | > 500ms for single queries |
| Row count | How many rows a query returns or scans | Scan count >> return count = missing index |
| Call count | How many times a method/query executes | Repeated identical queries = caching opportunity |
| RPC calls | Client-server round trips | High count = chatty design, consolidate calls |
Scenario: Elena traces the slow production report
PacificForgeβs production report takes 12 minutes to run. Elena captures a trace and finds:
| Finding | Duration | Root cause | Fix |
|---|---|---|---|
| SELECT FROM ProdTable scans entire table | 6.2s | No index on SchedDate column | Add non-clustered index on SchedDate |
| processReport() calls fetchCost() 45,000 times | 4.1s | Row-by-row lookup in a loop | Replace with set-based join using insert_recordset |
| Same CustTable.AccountNum lookup repeated 3,000 times | 1.8s | No caching on CustTable | Already has Found caching β query uses non-indexed field. Fix the WHERE clause to use primary key |
After fixes: 12 minutes β 45 seconds.
βThe trace told us exactly where to look,β Elena tells Sophie. βWithout it, weβd have been guessing.β
Query plan analysis
Reading SQL Server execution plans
When Trace Parser reveals a slow query, the next step is examining the execution plan to understand why:
| Plan element | Good sign | Bad sign |
|---|---|---|
| Index Seek | Query uses an index efficiently | β |
| Index Scan | β | Reading entire index β usually means the WHERE clause doesnβt match the index |
| Table Scan | β | Reading every row β no usable index at all |
| Nested Loop Join | Small outer table, indexed inner table | Large tables on both sides = slow |
| Hash Join | Large unsorted datasets | Small datasets = overhead wasted on hashing |
| Sort | β | Sorting in-flight = missing index with the right sort order |
| Key Lookup | β | Index doesnβt cover all needed columns (consider INCLUDE columns) |
Exam tip: Index Seek vs Index Scan vs Table Scan
The exam frequently tests understanding of these three:
- Index Seek β β jumps directly to matching rows using the index B-tree. Fast: O(log n)
- Index Scan β οΈ β reads the entire index sequentially. Better than table scan but still reads all index pages
- Table Scan β β reads every row in the table. No index used at all. Always investigate
Pattern: βA query on InventTrans WHERE ItemId = βWIDGET-01β does a table scan. Whatβs missing?β β A non-clustered index on ItemId.
Index design
Index types in F&O
| Index Type | Structure | When to use | Limitation |
|---|---|---|---|
| Clustered | Determines the physical sort order of data on disk. One per table. | The most frequently used range query or the primary key | Only ONE per table β choose carefully |
| Non-clustered | Separate B-tree structure with pointers to data rows. Many per table. | Secondary lookup patterns (WHERE, JOIN conditions) | Too many indexes slow down INSERT/UPDATE operations |
| Unique | Enforces uniqueness + serves as an index | Primary keys and alternate keys that must be unique | Insert fails if duplicate value exists |
| Include columns | Non-key columns added to a non-clustered index leaf level | Cover queries to avoid key lookups (SELECT columns not in WHERE) | Increases index size on disk |
Index design guidelines
| Guideline | Why |
|---|---|
| Index the WHERE clause columns | The optimiser uses indexes to filter rows β unindexed columns force scans |
| Put the most selective column first | The column that eliminates the most rows should be the leading column |
| Include columns to cover the query | If the SELECT list includes columns not in the index, SQL does a key lookup. Add them as INCLUDE columns to avoid this |
| Donβt over-index | Each index is updated on every INSERT/UPDATE/DELETE. More indexes = slower writes |
| Match the clustered index to the most common query | The clustered index determines physical data order β range queries on the clustered key are very fast |
Vik designs indexes for a custom table
Vik creates indexes for a new PFQualityResults table at Axion Dynamics:
| Index | Type | Columns | Reasoning |
|---|---|---|---|
PFQualityResults_PK | Clustered unique | QualityOrderId, TestId | Primary key β most queries filter by quality order |
PFQualityResults_Item | Non-clustered | ItemId, TestDate INCLUDE Result, Status | Reports query by item and date, need result and status in output |
PFQualityResults_Status | Non-clustered | Status, TestDate | Dashboard queries filter by status within date range |
βThree indexes is usually the sweet spot for a table,β Vik says. βOne clustered for the PK, one or two non-clustered for the most common query patterns. More than five and your writes start suffering.β
Optimising data entity imports
Data Management Framework (DMF) imports can be slow for large datasets. Key optimisation levers:
| Technique | How it helps | Setting |
|---|---|---|
| Set-based processing | Uses insert_recordset instead of row-by-row | Entity property: EnableSetBasedProcessing = Yes |
| Skip validation | Skips business rule validation for pre-validated data | Import settings: skip validation checkbox |
| Skip staging | Bypasses the staging table (direct insert) | Composite entity or skip staging option |
| Parallel processing | Multiple threads process different chunks simultaneously | Data Management β Configure entity execution parameters |
| Batch size tuning | Optimal number of rows per commit | Default 100 β increase to 500-1000 for clean data |
| Disable change tracking | Avoids change-tracking overhead during bulk load | Temporarily disable, re-enable after import |
Scenario: Elena optimises a product data import
PacificForge needs to import 200,000 product records from their legacy system. Initial import takes 18 hours.
Elenaβs optimisations:
| Change | Before | After |
|---|---|---|
| Enable set-based processing | Row-by-row inserts | Bulk insert_recordset |
| Skip staging table | Staging β Target (2 writes) | Direct to target (1 write) |
| Increase batch size to 500 | 100 rows per commit | 500 rows per commit |
| Enable 4 parallel threads | Single-threaded | 4 parallel workers |
| Disable change tracking | Tracking every insert | Disabled during import |
Result: 18 hours β 1.5 hours.
βSet-based processing alone cut it by 60%,β Elena tells DBA Harpreet. βThe parallel threads gave us another 3x improvement.β
Batch process optimization
Batch framework basics
| Concept | Detail |
|---|---|
| Batch job | A scheduled task that runs on the batch server (AOS) |
| Batch task | Individual units of work within a batch job |
| Batch group | Assigns tasks to specific AOS instances for load balancing |
| Recurrence | Schedule: once, daily, hourly, custom cron-like patterns |
| Parallelism | Multiple tasks within a job can run simultaneously on different threads |
Async framework
The Async framework allows batch tasks to run asynchronously β the calling code doesnβt wait for completion:
// Create an async batch task
SysOperationServiceController controller =
new SysOperationServiceController(
classStr(PFInventValuationService),
methodStr(PFInventValuationService, processWarehouse));
controller.parmExecutionMode(SysOperationExecutionMode::Async);
controller.startOperation();
// Continues immediately β task runs in background
| Benefit | Detail |
|---|---|
| Non-blocking | Main thread continues while async work runs in background |
| Scalable | Multiple async tasks can run in parallel across batch servers |
| Resilient | Each task can fail independently without crashing the parent job |
Sandbox framework
The Sandbox framework provides isolated execution for untrusted or resource-intensive code:
| Feature | Detail |
|---|---|
| Isolation | Code runs in a separate AppDomain with restricted permissions |
| Resource limits | Memory and CPU caps prevent a single task from consuming all resources |
| Timeout | Tasks are killed if they exceed the configured time limit |
| Use cases | External data transformations, complex calculations, third-party library calls |
Batch optimisation patterns
| Pattern | Description | When to use |
|---|---|---|
| Task splitting | Break one large task into many small parallel tasks | Large dataset processing (e.g. inventory valuation per warehouse) |
| Batch bundling | Group related small tasks into one execution | Many tiny tasks with per-task overhead |
| Dependency chains | Task B runs only after Task A completes | Sequential processing requirements |
| Priority assignment | Critical tasks get higher priority in the batch queue | Mix of urgent and background tasks |
| Server affinity | Pin tasks to specific AOS instances via batch groups | Resource-intensive tasks on dedicated servers |
Scenario: Elena redesigns the nightly batch
PacificForgeβs nightly processing runs 12 batch tasks sequentially β total time: 6 hours. Elena redesigns:
BEFORE (Sequential β 6 hours):
Task 1 β Task 2 β Task 3 β ... β Task 12
AFTER (Parallel with dependencies β 2 hours):
ββ Task 1 (Inventory sync) βββ
ββ Task 2 (Sales aggregation) ββββΆ Task 7 (Financial posting)
ββ Task 3 (Purchase reconcile) β β
ββ Task 4 (Quality processing) βββ βΌ
Task 12 (Reports)
ββ Task 5 (Production costing) βββ
ββ Task 6 (Warehouse movements) βββ΄βββΆ Task 8 (Cost roll-up)
ββ Task 9 (Email notifications)
ββ Task 10 (Data archival) β independent, run anytime
ββ Task 11 (Cache warm-up)Key changes:
- Tasks 1-4 run in parallel (no dependencies between them)
- Task 7 waits for Tasks 1-4 (dependency chain)
- Tasks 9-11 are independent β run in parallel with everything
- Each task assigned to a batch group for server affinity
βParallelism gave us 3x improvement. Identifying which tasks could run independently was the real insight,β Elena says.
Performance checklist summary
Elena's performance checklist for PacificForge
Elena creates a standard performance checklist for the development team:
Before writing code:
- Use set-based operations (insert_recordset, update_recordset, delete_from) wherever possible
- Choose TempDB temp tables for datasets > 500 rows
- Design indexes for your query patterns before building forms
After writing code:
- Run Trace Parser on key scenarios (form load, batch jobs, reports)
- Check for table scans in SQL analysis β add indexes
- Look for repeated identical queries β add caching
- Look for row-by-row loops that could be set-based
For batch jobs:
- Identify independent tasks and run them in parallel
- Set up dependency chains only where truly required
- Assign batch groups for server affinity
- Monitor execution times and set alerts for degradation
For data imports:
- Enable set-based processing on entities
- Tune batch size (500-1000 for clean data)
- Enable parallel threads
- Skip staging if data is pre-validated
Elena's Trace Parser analysis shows a query on InventTrans that takes 32 seconds and scans 2 million rows but only returns 150. What is the most likely root cause?
Vik needs to optimise a data import of 500,000 vendor records. The import currently takes 24 hours. Which combination of settings will have the biggest impact?
PacificForge runs 10 nightly batch tasks sequentially (total: 5 hours). Elena identifies that tasks 1-4 have no dependencies on each other. What should she do?
A non-clustered index on PFQualityResults has columns (ItemId, TestDate). A report queries SELECT ItemId, TestDate, Result, Status WHERE ItemId = 'WIDGET' AND TestDate > '2025-01-01'. The query does an Index Seek but is still slow due to key lookups. What should Elena add?
Next up: Youβve completed Domain 7! Review security, performance, and optimisation concepts, then test your knowledge across all MB-500 domains.