Priority events
Patterns, Performance, And API
Use priority-event recipes, metrics, pitfalls, performance notes, defaults, and quick reference.
- Patterns
- Metrics
- Pitfalls
- API reference
Use this page for recipes, pitfalls, capacity tuning, metrics, and signatures after the priority model is clear.
Capacity and Memory Management
Each priority event exposes:
int count = evt.Count;
int capacity = evt.Capacity;
bool hasSubscribers = evt.HasSubscribers;
evt.EnsureCapacity(128);
evt.TrimExcess();
evt.Clear();EnsureCapacity
EnsureCapacity(capacity) reserves capacity in every priority slot.
CombatBus.Tick.EnsureCapacity(256);Use it when you know the expected subscriber count and want to reserve storage before gameplay dispatch.
TrimExcess
TrimExcess asks all slots to compact their storage.
CombatBus.Tick.TrimExcess();Unlike plain AzEvent.TrimExcess, the current priority slot implementation does not intentionally reassign all live token identities during trim. Still, treat trim as a maintenance operation, not a per-frame habit.
Clear
Clear removes all subscriptions from all 9 slots.
CombatBus.Tick.Clear();Use it only when the event owner is resetting the whole event. Component owners should normally dispose their own tokens.
Metrics
Priority events implement IAzEventMetricsProvider and aggregate metrics across all 9 slots.
AzEventMetrics metrics = CombatBus.Tick.GetMetrics();
Debug.Log(metrics.ToString());
Debug.Log($"Total subscribers: {metrics.TotalSubscribers}");
Debug.Log($"Active subscribers: {metrics.ActiveSubscribers}");
Debug.Log($"Paused subscribers: {metrics.PausedSubscribers}");
Debug.Log($"Capacity: {metrics.Capacity}");
Debug.Log($"Fragmentation: {metrics.Fragmentation:P1}");
Debug.Log($"Invocations: {metrics.TotalInvocations}");
Debug.Log($"Listeners called: {metrics.TotalListenersCalled}");
Debug.Log($"Exceptions: {metrics.TotalExceptions}");Reset all metrics:
CombatBus.Tick.ResetMetrics();Reset runtime counters while keeping structural state:
CombatBus.Tick.ResetRuntimeMetrics();Metrics are useful for spotting:
- too many subscribers
- paused subscriptions that were never resumed
- fragmentation after high churn
- exception-heavy event chains
- excessive off-thread operation processing
Common Patterns
Ordered Gameplay Phases
public static readonly AzPriorityEvent<CombatContext> CombatResolved = new();
private void OnEnable()
{
CombatBus.CombatResolved.Subscribe(Validate, slot: 1).AddTo(_subscriptions);
CombatBus.CombatResolved.Subscribe(ApplyDamage, slot: 4).AddTo(_subscriptions);
CombatBus.CombatResolved.Subscribe(UpdateUi, slot: 8).AddTo(_subscriptions);
CombatBus.CombatResolved.Subscribe(RecordAnalytics, slot: 9).AddTo(_subscriptions);
}Early-Only Dispatch
CombatBus.Tick.InvokeSlots(AzPriorityMasks.UpTo(3));Late-Only Dispatch
CombatBus.Tick.InvokeSlots(AzPriorityMasks.From(7));Debugging Handler Failures
CombatBus.Tick.ExceptionMode = AzExceptionMode.Isolate;
AzInvokeResult result = CombatBus.Tick.InvokeSafe();
if (result.HasExceptions)
{
Debug.LogError(result.ToDetailedString(includeStackTraces: true));
}Pooled Listener
private readonly AzEventTokenBag _subscriptions = new();
private void Awake()
{
CombatBus.Tick.Subscribe(OnTick, slot: 5).AddTo(_subscriptions);
}
private void OnEnable()
{
_subscriptions.ResumeAll();
}
private void OnDisable()
{
_subscriptions.PauseAll();
}
private void OnDestroy()
{
_subscriptions.Dispose();
}Common Pitfalls
Using Add When You Need Cleanup
Bad for MonoBehaviours:
private void OnEnable()
{
CombatBus.Tick.Add(OnTick, slot: 5);
}Better:
private void OnEnable()
{
CombatBus.Tick.Subscribe(OnTick, slot: 5).AddTo(_subscriptions);
}
private void OnDisable()
{
_subscriptions.DisposeAll();
}Depending on Same-Slot Order
Bad:
evt.Subscribe(Prepare, slot: 5);
evt.Subscribe(ConsumePreparation, slot: 5);Better:
evt.Subscribe(Prepare, slot: 4);
evt.Subscribe(ConsumePreparation, slot: 5);Forgetting That Slot Values Are Strict
Slots are 1..9. Slot 0 and slot 10 throw.
evt.Subscribe(Handler, slot: 0); // invalid
evt.Subscribe(Handler, slot: 10); // invalidStoring an Off-Thread Subscribe Token
Off-thread Subscribe returns AzEventToken.Invalid.
Bad:
Task.Run(() =>
{
CombatBus.Tick.Subscribe(OnTick, slot: 5).AddTo(_subscriptions);
});Better:
private void OnEnable()
{
CombatBus.Tick.Subscribe(OnTick, slot: 5).AddTo(_subscriptions);
}Or, if off-thread queuing is intentional, do not expect a usable token from that call.
Invoking from a Worker Thread
Subscription operations can be queued from worker threads. Invocation should be main-thread owned.
Bad:
Task.Run(() => CombatBus.Tick.Invoke());Better:
private void Update()
{
CombatBus.Tick.Invoke();
}Using InvokeFast With Untrusted Handlers
InvokeFast is for trusted hot paths. If handler exceptions need to be isolated or reported through AzInvokeResult, use InvokeSafe.
// Trusted hot path
evt.InvokeFast();
// Diagnostic or untrusted path
AzInvokeResult result = evt.InvokeSafe();Overusing InvokeSafe in Hot Paths
InvokeSafe is valuable, but it is not the first choice for high-frequency trusted events.
// Diagnostics
AzInvokeResult result = evt.InvokeSafe();
// Hot path
evt.InvokeFast();Calling Dispose on a Reusable Bag in OnDisable
Bad:
private void OnDisable()
{
_subscriptions.Dispose();
}Good:
private void OnDisable()
{
_subscriptions.DisposeAll();
}Use Dispose() when the bag itself is done forever.
Performance Notes
The priority backend is designed for zero-allocation hot invocation after warmup.
Good defaults:
- Create events up front.
- Subscribe during setup or lifecycle transitions.
- Use slot
5for normal priority. - Use lower slots only when earlier execution matters.
- Use higher slots for observers and late reactions.
- Use
Invokeas the normal all-slot path when handlers are trusted. - Use
InvokeFastfor measured hot paths with trusted handlers. - Use
InvokeSafefor diagnostics, validation, and exception isolation. - Use token bags for lifecycle cleanup.
- Use pause/resume for pooled objects.
- Avoid per-frame subscribe/dispose churn.
Selective invocation can be cheaper than invoking all slots when only some phases need to run:
evt.InvokeSlots(AzPriorityMasks.UpTo(3));Use masks for fixed slot groups. Use InvokeWhere only when you need dynamic predicate logic.
API Quick Reference
AzPriorityMasks
public const ushort Slot1;
public const ushort Slot2;
public const ushort Slot3;
public const ushort Slot4;
public const ushort Slot5;
public const ushort Slot6;
public const ushort Slot7;
public const ushort Slot8;
public const ushort Slot9;
public const ushort OddSlots;
public const ushort EvenSlots;
public const ushort None;
public const ushort All;
public static ushort Build(int start, int end);
public static ushort UpTo(int end);
public static ushort From(int start);
public static bool ContainsSlot(ushort mask, int slot);
public static ushort Combine(params ushort[] masks);AzPriorityEvent
public AzExceptionMode ExceptionMode { get; set; }
public int Count { get; }
public int Capacity { get; }
public bool HasSubscribers { get; }
public void EnsureCapacity(int capacity);
public void Clear();
public void TrimExcess(int minCapacity = 0);
public void Add(Action h);
public void Add(Action h, byte slot);
public bool AddUnique(Action h);
public bool AddUnique(Action h, byte slot);
public AzEventToken Subscribe(Action h);
public AzEventToken Subscribe(Action h, byte slot);
public IDisposable SubscribeUnmanaged(Action h, byte slot = 5);
public bool Remove(in AzEventToken tok);
public bool Pause(in AzEventToken tok);
public bool Resume(in AzEventToken tok);
public void Invoke(AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeFast(AzCancelMode cancelMode = AzCancelMode.Soft, CancellationToken ct = default);
public AzInvokeResult InvokeSafe(AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void Invoke(out AzCancelInfo cancelInfo, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafe(out AzCancelInfo cancelInfo, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeSlots(ushort slotMask, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeSlots(ushort slotMask, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokePriorityAtLeast(byte minPriority, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafePriorityAtLeast(byte minPriority, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokePriorityAtMost(byte maxPriority, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafePriorityAtMost(byte maxPriority, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeSlot(byte slot, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeSlot(byte slot, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeOnly(ReadOnlySpan<byte> slots, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeOnly(ReadOnlySpan<byte> slots, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeWhere(Func<byte, bool> slotPredicate, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeWhere(Func<byte, bool> slotPredicate, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzEventMetrics GetMetrics();
public void ResetMetrics();
public void ResetRuntimeMetrics();AzPriorityEvent<T>
public AzExceptionMode ExceptionMode { get; set; }
public int Count { get; }
public int Capacity { get; }
public bool HasSubscribers { get; }
public void EnsureCapacity(int capacity);
public void Clear();
public void TrimExcess(int minCapacity = 0);
public void Add(Action<T> h);
public void Add(Action<T> h, byte slot);
public bool AddUnique(Action<T> h);
public bool AddUnique(Action<T> h, byte slot);
public AzEventToken Subscribe(Action<T> h);
public AzEventToken Subscribe(Action<T> h, byte slot);
public void Subscribe(Action<T> h, out AzEventToken tok, byte slot = 5);
public IDisposable SubscribeUnmanaged(Action<T> h, byte slot = 5);
public void SubscribeUnmanaged(Action<T> h, out IDisposable d, byte slot = 5);
public bool Remove(in AzEventToken tok);
public bool Pause(in AzEventToken tok);
public bool Resume(in AzEventToken tok);
public void Invoke(T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeFast(T arg, AzCancelMode cancelMode = AzCancelMode.Soft, CancellationToken ct = default);
public AzInvokeResult InvokeSafe(T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void Invoke(T arg, out AzCancelInfo cancelInfo, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafe(T arg, out AzCancelInfo cancelInfo, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeSlots(ushort slotMask, T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeSlots(ushort slotMask, T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeSlot(byte slot, T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeSlot(byte slot, T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzEventMetrics GetMetrics();
public void ResetMetrics();
public void ResetRuntimeMetrics();AzPriorityEventRef<T>
public sealed class AzPriorityEventRef<T>;
public AzExceptionMode ExceptionMode { get; set; }
public int Count { get; }
public int Capacity { get; }
public bool HasSubscribers { get; }
public void EnsureCapacity(int capacity);
public void Clear();
public void TrimExcess(int minCapacity = 0);
public void Add(AzRefHandler<T> h);
public void Add(AzRefHandler<T> h, byte slot);
public bool AddUnique(AzRefHandler<T> h);
public bool AddUnique(AzRefHandler<T> h, byte slot);
public AzEventToken Subscribe(AzRefHandler<T> h);
public AzEventToken Subscribe(AzRefHandler<T> h, byte slot);
public IDisposable SubscribeUnmanaged(AzRefHandler<T> h, byte slot = 5);
public bool Remove(in AzEventToken tok);
public bool Pause(in AzEventToken tok);
public bool Resume(in AzEventToken tok);
public void Invoke(in T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeFast(in T arg, AzCancelMode cancelMode = AzCancelMode.Soft, CancellationToken ct = default);
public AzInvokeResult InvokeSafe(in T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void Invoke(in T arg, out AzCancelInfo cancelInfo, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafe(in T arg, out AzCancelInfo cancelInfo, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeSlots(ushort slotMask, in T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeSlots(ushort slotMask, in T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public void InvokeSlot(byte slot, in T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzInvokeResult InvokeSafeSlot(byte slot, in T arg, AzCancelMode cancelMode = AzCancelMode.Soft, bool perHandlerChecks = false, CancellationToken ct = default);
public AzEventMetrics GetMetrics();
public void ResetMetrics();
public void ResetRuntimeMetrics();Supporting Types
public enum AzExceptionMode : byte
{
Isolate = 0,
PropagateAggregate = 1,
Abort = 2,
Silent = 3
}
public enum AzCancelMode : byte
{
None = 0,
Soft = 1,
Hard = 2
}
public readonly struct AzCancelInfo
{
public readonly bool WasCanceled;
public readonly int SlotIndex;
public readonly int HandlerIndex;
}Recommended Defaults for New Users
Use these defaults until you have a reason to specialize:
- Use
AzPriorityEvent<T>for most payload events. - Use
AzPriorityEventRef<T>only for large struct payloads. - Put ordinary gameplay handlers in slot
5. - Use slots
1..3for earlier critical work. - Use slots
7..9for UI, analytics, and late observers. - Prefer
Subscribe(...).AddTo(bag)overAdd(...). - Use
DisposeAll()inOnDisablefor normal components. - Use
PauseAll()andResumeAll()for pooled objects that enroll once. - Use
Invokefirst; move toInvokeFastfor measured hot paths. - Use
InvokeSafewhen you need result details or exception diagnostics. - Keep invocation on the main thread.
- Create subscriptions on the main thread unless you intentionally need queued off-thread subscription behavior.
- Do not rely on same-slot ordering for correctness.
Complete Example
using Azkar.Eda;
using UnityEngine;
public static class BattleBus
{
public static readonly AzPriorityEvent<BattleFrame> BattleFrameResolved = new();
}
public readonly struct BattleFrame
{
public readonly int FrameId;
public readonly int PlayerHealth;
public BattleFrame(int frameId, int playerHealth)
{
FrameId = frameId;
PlayerHealth = playerHealth;
}
}
public sealed class BattleDriver : MonoBehaviour
{
private int _frameId;
private int _playerHealth = 100;
private void Awake()
{
BattleBus.BattleFrameResolved.ExceptionMode = AzExceptionMode.Isolate;
}
private void Update()
{
var frame = new BattleFrame(_frameId++, _playerHealth);
BattleBus.BattleFrameResolved.InvokeFast(frame);
}
}
public sealed class BattleSystems : MonoBehaviour
{
private readonly AzEventTokenBag _subscriptions = new();
private void OnEnable()
{
BattleBus.BattleFrameResolved.Subscribe(ValidateFrame, slot: 1).AddTo(_subscriptions);
BattleBus.BattleFrameResolved.Subscribe(ApplyGameplay, slot: 5).AddTo(_subscriptions);
BattleBus.BattleFrameResolved.Subscribe(UpdateHud, slot: 8).AddTo(_subscriptions);
BattleBus.BattleFrameResolved.Subscribe(RecordAnalytics, slot: 9).AddTo(_subscriptions);
}
private void OnDisable()
{
_subscriptions.DisposeAll();
}
private void ValidateFrame(BattleFrame frame)
{
// Runs before gameplay.
}
private void ApplyGameplay(BattleFrame frame)
{
// Normal priority.
}
private void UpdateHud(BattleFrame frame)
{
// Late presentation.
}
private void RecordAnalytics(BattleFrame frame)
{
// Last observer.
}
}This example shows the basic priority-event ecosystem:
BattleBusowns the event.BattleDriverinvokes it.BattleSystemssubscribes multiple handlers to different slots.- Slot order expresses phase order.
AzEventTokenBagowns cleanup.InvokeFastis used in a hot frame path after setup.
