| name | performance-hot-paths |
| description | Use when writing or reviewing code on the combat or item hot path, declaring an effect Affinity, touching the Sink/Dispatcher, the ItemView cache, interning, or anything the ArchUnit/CI lint or the JMH bench guards. |
Performance hot paths
The combat hit is an allocation-light array walk over primitives (§8), enforced by a gate,
not by hope. The loop itself (WornState.combatAttack/Defense int[] → Ability[] → gates →
effects → flush) lives in effect-engine; this skill is the budget it runs under. Cold paths
(load/reload/menus/commands/item-apply) are unconstrained — parse and allocate freely there.
On the hit: no string ops, no boxing, no item re-read, no map lookups, no YAML/DSL parse. Damage
folds once (§6.1) — never event.setDamage from an effect; contribute a delta.
Banned from the inner loop (the lint enforces these — §8, §11)
Banned in se-engine hot-path packages | Use instead |
|---|
Bukkit.getScheduler() / direct entity mutation | declare an Affinity; emit a Sink intent (§3.5–3.6) |
new NBTItem, ItemStack#clone, Gson, NBT clone | cache.of(stack): one blob-key lookup (§5.2) |
String#split, regex compile, YAML/DSL parse, map lookups | compiled at load; dense-int indices at runtime — effect/selector dispatch is a kindId array index, not a head lookup (ADR-0039) |
| string ops / boxing in conditions or effect args | thread-local primitive FactBuffer by slot, populated demand-driven per the ability's FactMask (§3.4, ADR-0039) |
Interning: every name (enchant/group/world/material/potion/sound) is a dense int at
runtime; stable strings exist only at the PDC boundary (§5.3, §8). See item-data-model.
Read once, resolve once
- Item read = one map lookup by the raw combat blob; miss = one compact decode. Key is
the full blob string + generation counter (collision-safe; reload swaps a fresh per-gen
map, so growth is bounded by distinct gear configs) — NOT ItemMeta identity, which misses
(copy-on-write) and can alias (§5.2). A helmet hit 20×/sec decodes once.
- Worn-set resolve runs once per equip event, never per hit; the result is immutable,
multi-set, pre-flattened per direction — set/omni/crystal cost nothing per hit (§5.5). The
victim path reads only this snapshot, never the live
ItemStack. The same resolve folds a
per-trigger FactMask (the union of the trigger's abilities' referenced fact slots), so the
populator later computes only those slots — an unread %nearbyenemies%/%distance%/%groundblock%
costs nothing (ADR-0039).
Scheduler-hop budget (Affinity — §3.6)
Declared Affinity folds to ability level at compile time (the SPI rule is in effect-engine);
the per-hop cost is the perf concern here:
| Affinity | Hops (Paper + Folia) |
|---|
CONTEXT_LOCAL (DAMAGE, REDUCTION, POTION:self, MESSAGE, SOUND) | 0 — inline on the firing region thread |
TARGET_ENTITY / REGION / AOE (cross-region, AoE) | ~1 per distinct target thread, batched |
| defense-side victim mutation (heal-on-hit, dodge, warp) | 1 hop on Folia — stated honestly, not "zero" |
Dynamic victim facts (%victim health%, pose) are captured at event entry on the firing region or
from the immutable WornState; never a live cross-region victim read (§3.4). See folia-scheduling.
Deferred/cross-region intents must snapshot the primitives they need into an immutable carrier
before the plan flushes, so a pooled intent is never aliased after run returns (§3.6).
Enforcement — the number is the spec (§8, §11)
These are LIVE build gates, not conventions:
- The engine ArchUnit boundary rejects every banned symbol above
(
Bukkit.getScheduler(), direct entity mutation, ItemStack#clone/Gson/NBT clone,
String#split/regex/YAML parse, string ops/boxing in conditions) inside the
se-engine hot-path packages — a stray hot-path allocation or scheduler hop is a
./gradlew build failure, caught before review.
- A JMH bench asserts ~0 steady-state allocation on the per-hit pipeline and a
throughput floor (incl.
gateWalkRecorded — the always-on /se why record path,
ADR-0045); a regression fails the build. Verify on the real matrix too — see
matrix-gate.
Effects emit intents and never schedule, so an author cannot write a Folia or allocation bug — see
effect-engine. Interaction folds (damage, suppression) run once on single-thread-owned scratch
— see feature-interaction-rules. General invariants: starenchants-conventions.