Skip to content

perf(aurakit): check the build budget before a job, not after it - #2010

Open
JuJuFX-dev wants to merge 1 commit into
EllesmereGaming:mainfrom
JuJuFX-dev:perf/aurakit-budget-precheck
Open

perf(aurakit): check the build budget before a job, not after it#2010
JuJuFX-dev wants to merge 1 commit into
EllesmereGaming:mainfrom
JuJuFX-dev:perf/aurakit-budget-precheck

Conversation

@JuJuFX-dev

Copy link
Copy Markdown
Contributor

What does this PR do?

The shared aura build queue in EllesmereUI_AuraKit.lua drains with a per-frame time budget, but the budget was tested after each job had already run rather than before starting one. A frame could therefore cost the budget plus one entire job, and this PR removes that overshoot. No job is made slower or faster by it: the same jobs run in the same order and do the same work, they are just no longer allowed to stack past the budget within one frame.

Worked through with the measured costs of the nameplate bundle jobs, against the 8 ms in-combat budget:

before   np:debuffs 5.9 ms runs, elapsed 5.9 is under 8 so the loop continues,
         np:buffs 8.9 ms runs as well, the frame ends at 14.8 ms, and only
         then does the check fire
after    np:debuffs 5.9 ms runs, np:buffs is estimated at 8.9 which no longer
         fits in what is left of the budget, so it waits for the next frame;
         the frame ends at 5.9 ms

The 8.9 ms is what that job costs either way. What changes is whether it is allowed to land on top of a frame that is already most of the way through its budget. This matters whenever the bundle pool has to grow during a fight, and a 14.8 ms frame is a dropped one at 85 fps.

Each job label now carries a running estimate of its own cost, taken from the job's elapsed time, and a job only starts if it still fits in the remaining budget. Two rules keep that safe. The first job of a frame always runs whatever it costs, because a single job can exceed the whole combat budget and a strict check would leave it at the head of the queue forever; since the flag resets every frame, any job at the head is at most one frame from running, which rules out starvation structurally rather than by argument. A label with no estimate yet counts as possibly expensive and waits for a fresh frame, because letting it through leaves the very first occurrence of a label at the full cost, which is the exact case this change exists for.

For the player this is fewer micro-hitches when a lot of enemy nameplates appear at once. There is no new setting, no visual change, and no change to what work is done: the pre-check returns before the queue entry is consumed, so the order of jobs, the set of jobs and their content are all identical, and only the frame a job runs in can move. The old code already split jobs across frames whenever the budget was hit, so no new state is reachable for any consumer of the queue.

The trade is throughput in the gentle 8 ms regime. Three bundles drain in twelve frames instead of six, so plate auras during pool growth land roughly 70 ms later at 85 fps. The 250 ms login window is untouched by design.

How was it tested?

Tested in game on live, in a follower dungeon with the same large chained pull before and after. The addon's worst frame dropped from 14.785 ms to 6.383 ms, and the breakdown is the part that matters: before, the worst frame carried two build jobs, and after it carries exactly one, which is precisely what the pre-check specifies. Expressed as a ratio inside each run, so that run-to-run variation cannot flatter it, the worst frame went from 1.66 times the largest single job to exactly 1.00 times.

Both the nameplate and the RaidFrames consumers of the queue were exercised in play. EUI_RaidFrames_AuraContainers.lua serves raid, party and extra unit buttons from one code path, so the party frames present throughout the test runs built and populated their aura containers through the same rf:* jobs and the same job bodies; a raid-sized group multiplies how many of those jobs are queued but adds no new one. Player Aura Bars is not affected at all, since it never touches the queue and builds through the synchronous AK.RequestContainer path. The CooldownManager consumer was not exercised and was checked by reading instead: both of its queue sites, like every RaidFrames one, open with an existence guard that re-validates its own precondition rather than assuming a sibling job ran in the same frame.

Checklist

  • New settings default OFF (no behavior change without opt-in) - N/A, no new setting; this is a scheduling fix with no opt-in surface
  • Zero cost while disabled: no events registered, no polling, no hooks doing work, no frames built - N/A, nothing is added that can be disabled; no new events, hooks or frames
  • Cheap while enabled: event-driven (no polling, no timer-based logic, no per-frame allocations) - the worker's debugprofilestop calls per iteration did not increase, the pre-check reuses the previous iteration's timestamp, and the only added state is one small table keyed by the fixed set of job labels
  • No writes onto Blizzard-owned frames (weak-table pattern used); HookScript/hooksecurefunc only, never SetScript on Blizzard frames - N/A, no Blizzard frame is touched
  • Tested in-game on live; no version gates or pre-Midnight APIs added

The build worker's budget test sat after RunJob, so a frame cost the budget PLUS one whole job. Measured on 2026-09-07 with /euiakprof during plate aura pool growth: np:debuffs at 5.869 ms fit under the 8 ms combat budget, the loop continued, np:buffs added 8.916, and the frame landed at 14.785 ms. That was 98.7 % of the addon's worst frame in a 127 s run, and euidiag cannot see it at all because it samples every 0.1 s.

The queue order in QueueBundleBuild is debuffs, buffs, cc+pool, which is exactly the adjacent pair the peak frame contained.

Each label now carries a running cost estimate, taken from the job's own elapsed time, and a job only starts if it still fits. Two rules keep that safe:

The first job of a frame always runs, whatever it costs. np:buffs alone exceeds the 8 ms combat budget, so a strict check would leave it at the head of the queue forever. Since the flag resets every frame, any job at the head is at most one frame from running, which rules out starvation structurally rather than by argument.

A label with no estimate yet counts as possibly expensive and waits for a fresh frame. Letting it through was the first version and it did not fix the cold case at all: the first occurrence of a label still produced the full 14.785 ms frame, which is the exact case this change exists for.

The estimate rises to a new maximum immediately and decays at a quarter of the gap, because a cheap sample must not license a spike on the next frame.

Verified before any in-game test by porting the drain loop, old and new, and replaying it with the measured job costs and the real queue order:

  budget 8 ms, 3 bundles          old 6 frames / worst 14.785    new 12 frames / worst 8.916
  budget 250 ms, 16 bundles       old 2 frames / worst 251.3     new  4 frames / worst 249.2
  unknown 20 ms job, 8 ms budget  drains one job per frame, no starvation

So the worst frame drops 40 % and is now bounded by the largest single job rather than the largest pair. The login turbo window is untouched by design. The price is throughput in the gentle regime: three bundles drain in twelve frames instead of six, so plate auras during growth land about 70 ms later at 85 fps.

debugprofilestop calls did not increase. The pre-check reuses the previous iteration's timestamp and the post-check reuses the job's own, so it is one call per job where it used to be one per loop iteration.

All 26 QueueBuildJob sites in the suite pass a label, so the unlabelled fallback bucket is defensive only. The entry is not consumed before the pre-check returns, so a deferred job stays at the head and cannot be dropped.

Not measured in game yet, and the raid-side consumers cannot be A/B-ed with the content available here; they are covered by inspection and by the checklist instead.

Shared file touched with prior sign-off: EllesmereUI_AuraKit.lua.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant