Acumatica · Performance

Acumatica Performance — Graph Event Loop Optimisation

Acumatica Performance — Graph Event Loop Optimisation is the Acumatica performance topic that nobody asks about until they have to.

John Kihiu12 min read

Acumatica's graph is not asynchronous in the JavaScript sense — there is no promise queue — but it does have a real event loop of its own: RowSelecting, RowInserting, RowUpdated, FieldUpdated, and FieldVerifying fire in a defined order as PXCache processes a row, and each of those handlers can trigger more cache operations that queue more events. Understanding that loop is the difference between a snappy data entry screen and one where typing a quantity visibly pauses the UI.

The order that actually fires

For a single field edit in the UI, the practical sequence is: FieldVerifying (validation, can cancel) → FieldUpdating (value transformation) → cache value set → FieldUpdated (side effects: recalculating dependent fields) → RowUpdated (row-level consistency) → persistence events on save. Each FieldUpdated handler that calls Cache.SetValueExt on another field re-enters this same loop for that field before the outer handler returns. A chain of three fields each updating the next is three nested passes through the loop for one keystroke.

C# — A CASCADE THAT COSTS MORE THAN IT LOOKS
protected virtual void SOLine_Qty_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e)
{
    var row = (SOLine)e.Row;
    if (row == null) return;

    // Recalculates ExtPrice, which itself has a FieldUpdated
    // that recalculates line discount, which recalculates order total...
    cache.SetValueExt<SOLine.extPrice>(row, row.Qty * row.UnitPrice);
}

None of this is wrong by itself — it is exactly how dependent-field recalculation is supposed to work. It becomes a performance problem at scale: a grid paste of 200 lines fires this cascade 200 times, and if the order total recalculation at the end of the chain re-sums every line each time rather than incrementally, you get roughly O(n²) behavior on what should be a linear paste operation.

Spotting a runaway cascade

The Trace screen (or the Request Profiler's detailed trace) shows event handler execution as a nested, timestamped list. A healthy field update shows a shallow tree — two or three levels deep. A cascade shows the same handler names repeating at increasing depth, and the total time for a "simple" quantity change running into the hundreds of milliseconds is the tell.

Recalculating totals inside the loop instead of after it

The most common cause of quadratic cascades I have removed from client customizations: an order total recalculated by re-summing every line inside SOLine_Qty_FieldUpdated, rather than deferring the total recalculation to RowUpdated or, better, to a single pass after a multi-row paste completes. Move expensive aggregate work out of the per-field handler and into a point that fires once per save, not once per keystroke.

Breaking cascades deliberately

Two tools stop unnecessary re-entrancy:

Wrapping up

Treat FieldUpdated/RowUpdated chains as a real event loop with real re-entrancy cost, not a free hook to attach logic to. Trace the nesting depth before assuming a slow grid is a database problem, and move aggregate recalculation out of the per-keystroke path whenever the operation is really "recompute once at the end."

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.