agentsclimarketplace

Skill performance

Skill javiarmesto/ALDC-AL-Development-Collection/skills/skill-performance

AL performance optimization patterns for Business Central. Use when optimizing queries with SetLoadFields, working with FlowFields and CalcFields, profiling codeunits, or resolving performance issues.From its SKILL.md

Install
npx -y skills add javiarmesto/ALDC-AL-Development-Collection --skill skill-performance

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

11.1 KB, as published. Nobody here has run it

Skill: AL Performance Optimization

Purpose

Identify, analyze, and fix performance bottlenecks in AL code: inefficient queries, FlowField issues, loop anti-patterns, and data-volume problems in Business Central.

When to Load

This skill should be loaded when:

  • A page, report, or batch process is slow or timing out
  • CPU profiling is needed to identify hotspots
  • A static triage of the codebase is requested for performance issues
  • A feature involves large-dataset processing or high-frequency code paths
  • AL0896 (circular FlowField) errors appear
  • Architecture review requires performance analysis of a new design

Core Patterns

Pattern 1: SetLoadFields + Early Filtering

Always filter before finding, and load only needed fields. Order matters.

// ✅ Correct: SetRange first, SetLoadFields before Find
Item.SetRange("Third Party Item Exists", false);
Item.SetLoadFields("Item Category Code", Description);
if Item.FindSet() then
    repeat
        // Only "Item Category Code" and Description loaded from DB
    until Item.Next() = 0;

// ❌ Wrong: SetLoadFields after SetRange (ignored), loads all fields
Item.SetLoadFields("Item Category Code");
Item.SetRange("Third Party Item Exists", false);
Item.FindFirst();

// ❌ Wrong: No filter — full table scan
procedure GetCustomersByCity(CityFilter: Text): Integer
var
    Customer: Record Customer;
    Count: Integer;
begin
    if Customer.FindSet() then       // loads entire Customer table
        repeat
            if Customer.City = CityFilter then
                Count += 1;
        until Customer.Next() = 0;
end;

// ✅ Correct: Filter pushed to DB
procedure GetCustomersByCity(CityFilter: Text): Integer
var
    Customer: Record Customer;
begin
    Customer.SetRange(City, CityFilter);
    Customer.SetRange(Blocked, Customer.Blocked::" ");
    exit(Customer.Count());
end;

Pattern 2: Set-Based Aggregation (CalcSums / CalcFields)

Avoid manual loops for aggregation — push the sum to the database.

// ❌ Loop accumulation — N rows fetched and processed in AL
procedure GetTotalSales(CustomerNo: Code[20]): Decimal
var
    Entry: Record "Cust. Ledger Entry";
    Total: Decimal;
begin
    Entry.SetRange("Customer No.", CustomerNo);
    if Entry.FindSet() then
        repeat
            Total += Entry.Amount;
        until Entry.Next() = 0;
    exit(Total);
end;

// ✅ CalcSums — single aggregation query at DB level
procedure GetTotalSales(CustomerNo: Code[20]): Decimal
var
    Entry: Record "Cust. Ledger Entry";
begin
    Entry.SetRange("Customer No.", CustomerNo);
    Entry.CalcSums(Amount);
    exit(Entry.Amount);
end;

For FlowFields accessed outside a page context, always call CalcFields before reading:

Customer.SetLoadFields("Balance (LCY)");
Customer.Get(CustomerNo);
Customer.CalcFields("Balance (LCY)");   // required — not auto-calculated in code

Pattern 3: Temporary Tables / Dictionary / List

Pre-load data once, then process in-memory multiple times.

// ✅ Temporary table — structured record data, multi-pass processing
procedure ProcessSalesData(var TempSalesLine: Record "Sales Line" temporary)
var
    SalesLine: Record "Sales Line";
begin
    SalesLine.SetLoadFields("No.", Quantity, "Unit Price", Amount);
    if SalesLine.FindSet() then
        repeat
            TempSalesLine := SalesLine;
            TempSalesLine.Insert();
        until SalesLine.Next() = 0;

    // Process in-memory — zero additional DB hits
    ApplyDiscounts(TempSalesLine);
    CalculateTotals(TempSalesLine);
end;

// ✅ Dictionary — key-value lookup cache
procedure CacheCustomerNames(): Dictionary of [Code[20], Text]
var
    Customer: Record Customer;
    Cache: Dictionary of [Code[20], Text];
begin
    Customer.SetLoadFields("No.", Name);
    if Customer.FindSet() then
        repeat
            Cache.Add(Customer."No.", Customer.Name);
        until Customer.Next() = 0;
    exit(Cache);
end;

// ✅ List — simple value collection
procedure GetBlockedCustomerNos(): List of [Code[20]]
var
    Customer: Record Customer;
    Result: List of [Code[20]];
begin
    Customer.SetRange(Blocked, Customer.Blocked::All);
    Customer.SetLoadFields("No.");
    if Customer.FindSet() then
        repeat
            Result.Add(Customer."No.");
        until Customer.Next() = 0;
    exit(Result);
end;

Pattern 4: Batch Processing and Avoiding Nested DB Calls

Move database operations outside loops; batch writes.

// ❌ Nested DB call inside loop — O(n²) database hits
if MainTable.FindSet() then
    repeat
        OtherTable.SetRange(Field, MainTable.Field);
        if OtherTable.FindSet() then          // DB call per iteration
            repeat
                // process
            until OtherTable.Next() = 0;
    until MainTable.Next() = 0;

// ✅ Pre-load into temp table, then join in memory
if OtherTable.FindSet() then
    repeat
        TempOther := OtherTable;
        TempOther.Insert();
    until OtherTable.Next() = 0;

if MainTable.FindSet() then
    repeat
        if TempOther.Get(MainTable.Field) then
            // process — no DB hit
    until MainTable.Next() = 0;

Batch writes — collect changes, write once:

// ✅ Calculate all values first, then single Modify
procedure UpdateCustomerStats(CustomerNo: Code[20])
var
    Customer: Record Customer;
    TotalBalance: Decimal;
    LastPaymentDate: Date;
begin
    CalculateCustomerTotals(CustomerNo, TotalBalance, LastPaymentDate);

    Customer.SetLoadFields("Balance (LCY)", "Last Payment Date");
    if Customer.Get(CustomerNo) then begin
        Customer."Balance (LCY)" := TotalBalance;
        Customer."Last Payment Date" := LastPaymentDate;
        Customer.Modify(true);   // single write
    end;
end;

Scale-aware processing:

procedure UpdatePricesForItems(var Item: Record Item)
begin
    if Item.Count() > 1000 then
        UpdatePricesInBatches(Item)    // job queue / chunked
    else
        UpdatePricesDirectly(Item);
end;

Pattern 5: FlowField Optimization (AL0896)

Circular FlowField references cause infinite evaluation (AL0896 error):

❌ Circular dependency:
Table Customer: FlowField "Total Sales" → CalcFormula from Sales Statistics
Table Sales Statistics: FlowField "Customer Balance" → CalcFormula from Customer

Compiler error AL0896: recursive dependency detected

Resolution strategies (in order of preference):

  1. Break the chain — convert one FlowField to a regular field updated via trigger
  2. Use CalcSums — replace FlowField with explicit CalcSums in code
  3. Restructure — move the calculation to a dedicated codeunit called on demand
  4. SumIndexFields — use SIFT keys for frequently summed values

Avoid FlowFields:

  • With complex nested CalcFormula expressions
  • Called inside repeat...until loops (call CalcFields once, outside loop if possible)
  • Where the source table is large and unfiltered

Workflow

Step 1: Triage (Static Code Analysis)

Scan the codebase before profiling to identify structural issues:

Patterns to detect manually or with search + problems:

  • FindSet() / FindFirst() without preceding SetRange / SetFilter
  • SetLoadFields placed after SetRange (wrong order)
  • Database calls (Get, FindSet, FindFirst) inside repeat...until
  • CalcFields inside loops
  • FlowField CalcFormula referencing tables that reference back (AL0896)
  • Commit inside loops (locks + performance risk)
  • Missing keys for columns used in SetRange

Severity assessment:

  • 🔴 Critical — DB call in loop over large table, circular FlowField, timeout-causing query
  • 🟡 High — Missing SetLoadFields in high-frequency path, nested loops
  • 🟢 Medium — Suboptimal aggregation (loop instead of CalcSums)
  • 🔵 Low — Best-practice suggestion, minimal current impact

Step 2: Profile (Runtime Measurement)

Generate CPU profile for runtime bottleneck identification:

Capture a CPU profile in VS Code (VS Code command — not an agent tool)

Analyze profile for:

  • Hotspots — procedures with highest cumulative execution time
  • Frequency — procedures called most often (especially in loops)
  • DB operations — expensive FindSet / Get calls
  • FlowField evaluations — unexpectedly costly CalcFields

Compare before/after optimizations by re-profiling after each fix.

⚠️ Human Gate — cleanup: Before clearing codelenses, confirm all findings are documented:

Clear profile codelenses in VS Code  ← VS Code command (not an agent tool), only after approval

Step 3: Fix

Apply fixes in priority order (critical first). For each fix:

  1. Apply targeted change (Pattern 1–5 above)
  2. Rebuild: al_build
  3. Re-profile to verify improvement

Performance targets:

MetricTarget
Page load< 2 seconds
Report generation< 10 seconds (standard dataset)
API response< 500 ms
Batch processing> 1,000 records/minute

Step 4: Document Findings

For significant optimizations, create a triage report at .github/plans/perf-triage-<scope>.md:

# Performance Triage — <Scope>

**Date**: YYYY-MM-DD
**Analyzed**: [path or objects]

## Findings

| Severity | Location | Issue | Recommendation |
|---|---|---|---|
| 🔴 | `File.al:42` | Nested DB call in loop | Pre-load into TempTable |
| 🟡 | `File.al:87` | Missing SetLoadFields | Add SetLoadFields("No.", Name) |

## Changes Applied

1. [Change] — before/after metric
2. [Change] — before/after metric

## Remaining Items

[Issues not yet fixed, prioritized]

⚠️ Human Gate — report: Review findings before saving; confirm no sensitive code patterns are exposed.

References

Constraints

  • This skill covers analysis and fix patterns — it does NOT duplicate the passive rules in al-performance.instructions.md (auto-applied to all .al files)
  • Do NOT modify source code automatically during triage — generate report and get approval first
  • Do NOT clear profile codelenses without human confirmation
  • Do NOT save triage report without human gate review
  • For runtime debugging of a specific issue (breakpoints, snapshots) → load skill-debug.md
  • For event subscriber performance issues → load skill-events.md

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.