Fix/performance optimisations#1455
Merged
Merged
Conversation
hillerstorm
reviewed
Jul 16, 2026
hillerstorm
left a comment
There was a problem hiding this comment.
Adversarial review of the engine changes (results churn excluded). Two correctness bugs, one latent regression, and a handful of robustness/cleanup suggestions — details inline. The two bugs worth fixing before merge are the group-APL lhsType issue and the SEF clone-list mutation.
- Set lhsType/rhsType when rebuilding APLValueCompare/APLValueMath in replacePlaceholders, and add a regression test for the placeholder compare path - Cache operand types in APLValueMath to finish the Type() optimisation - Restore the aura expiry re-check lost in the batched advance() loop so same-tick OnExpire refresh chains (e.g. Arcane Missiles -> Arcane Charges) are not faded; several spec results move back to master values - Replace the fixed [16] stack buffer in advance() with a reusable scratch slice on auraTracker - Key aurasByLabel by string instead of maphash to rule out collisions - Iterate the stable pets slice in SEF CastCopySpell since deactivateClone compacts activeClones in place - Gate the launchPA guard on pa.cancelled instead of the NeverExpires sentinel in runicPowerBar.reset - Extract a shared evalGeneration cache helper for the dot increase APL values and document the evalGeneration bump contract
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Performance optimisations — sim core hot path
Targeted profiling pass across six benchmark specs (Frost DK, Unholy DK, Windwalker, Affliction, Demonology, Beast Mastery). No behaviour changes except one rollover bug fix noted below and one aura-expiry fix from review (see Review follow-ups).
Changes
sim/core/apl_action.go— IsReady / condition evaluation orderimpl.IsReady(sim)is now evaluated beforecondition.GetBool(sim). Spell-unavailability checks (cooldown, insufficient resources) are significantly cheaper than evaluating a condition expression tree, so 50–70% of priority-list entries short-circuit before any condition logic runs.sim/core/apl_values_dot.go— DoT APL value result cachingAPLValueDotPercentIncrease,APLValueDotCritPercentIncrease, andAPLValueDotTickRatePercentIncreasenow cache theirfloat64result perevalGeneration. These callExpectedTickDamage()(float math + snapshot lookup) on every evaluation; caching is high-value for Affliction and Unholy where the same dot-value node is checked multiple times per APL scan.sim/core/apl_values_operators.go—APLValueComparelhsType cachingAPLValueCompare.lhsTypeis now cached at construction time, eliminating a virtual dispatch call on every comparison evaluation.sim/core/spell.go+sim/core/target.go+sim/core/unit.go—expectedDamageCacheslice migrationThree per-
AttackTablemaps (map[*Spell]*ExpectedDamageCalculatorCache) replaced with per-spell slices indexed bytarget.UnitIndex. Eliminates pointer hashing on every DoT tick damage calculation and removes lazy allocation per new target. Caches now cleared inspell.reset()(per-iteration) rather than scattered acrossunit.reset().sim/core/runic_power.go— single reused rune PAThe rune regen
PendingActionis allocated once per DK lifetime and reused on every reschedule. Previously a new&PendingAction{...}plus closure was heap-allocated on each rune regen event. Also fixesmake([]int8, 0)→lastRegen[:0]to avoid a slice allocation per regen tick.sim/core/aura.go— two-pass aura expiryauraTracker.advance()now collects all expired auras into a reusable scratch slice in a single scan while simultaneously computingminExpiresfor survivors. A second pass re-checks expiry and callsDeactivateon each. The outer loop handles the rare case where aDeactivatecallback expires additional auras. Common case (no expirations at this timestamp) pays exactly one scan and returns immediately.sim/core/aura.go— O(1)GetAuralabel lookupAdded
aurasByLabel map[string]*AuratoauraTracker, populated inregisterAura(), mirroring the existingaurasByTagpattern.GetAuranow returnsat.aurasByLabel[label]directly instead of scanning the fullat.aurasslice (~200 entries per unit).sim/monk/ww_storm_earth_and_fire.go— SEF zero-alloc clone listsupdateActiveClones()previously calledmake([]*StormEarthAndFirePet, 0, 3)twice on every state change. Both slices are now pre-allocated in the constructor and reset with[:0]on each update. Also removed an earlybreakinCastCopySpellon nil spell lookup — changed to fall-through to avoid silently skipping remaining pets if spellbooks diverge in future.Review follow-ups (7c60e6c)
All review comments addressed:
advance()loop had dropped the per-aura expiry re-check that the oldgoto restartprovided, so an aura refreshed by an earlier same-tickOnExpire(e.g. Arcane Missiles expiry activating Arcane Charges) was wrongly faded. Restoring the guard moved six spec suites (Arcane, Frost Mage, Balance, Guardian, Brewmaster, Prot Paladin) back to master's exact expected results — the branch's earlier.resultsupdates had encoded the bug. Updated results files accordingly.replacePlaceholderscompare/math rebuilds now set the cachedlhsType/rhsType(the group-reference rebuild previously produced compares that always evaluated false). Added a regression test for the placeholder→compare path.APLValueMathnow caches both operand types at construction, finishing theType()/dispatch optimisation thatAPLValueComparestarted.aurasByLabelkeyed bystringinstead ofmaphashuint64 — Go AES-hashes string keys natively, so O(1) is kept without the (astronomically unlikely but silent) collision risk.CastCopySpelliterates the stablepetsslice, sincedeactivateClonecompactsactiveClonesin place under an in-flightrange.advance()scratch buffer is a reusable field onauraTrackerinstead of a fixed[16]stack array — no zeroing cost, no overflow protocol.launchPAguard is now gated on!rp.pa.cancelled, replacing theNextActionAt = NeverExpiressentinel inrunicPowerBar.reset().cached()helper (verified to fully inline, zero closure allocations) and theevalGenerationbump contract is documented on the field.Re-benchmarked after the fixes (Ryzen 9 9950X3D,
-benchtime=3s -count=6, benchstat, all p=0.002):Benchmark results
Two measurements, both vs unmodified master on an i5-13600K.
Micro-benchmarks (
go test -bench -benchtime=10s, empty equipment, 1 sim iteration × 300 s fight per op):