Runtime guidance

Performance And Threading

Choose clear defaults first, then use invoke variants, ref payloads, coalescing, and main-thread bridges where profiling calls for them.

  • Backend guidance
  • Invoke variants
  • Main-thread rules
  • Token lifecycle cost

Azkar EDA is designed to keep normal Unity event and state code clear first, then provide measured options for performance-sensitive paths. Start with the clear path by default, and use faster variants only when profiling shows the path matters.

Deep references:

Backend Guidance

AzEvent

Normal use

Discrete notification with no payload

Hot-path note

Cheapest shape and easiest to reason about.

AzEvent<T>

Normal use

Discrete notification with payload

Hot-path note

Good default for small payloads and normal gameplay code.

AzEventRef<T>

Normal use

Large struct payload

Hot-path note

Avoids copying a large struct to every handler.

AzPriorityEvent

Normal use

Ordered no-payload notification

Hot-path note

Use only when order matters.

AzPriorityEvent<T>

Normal use

Ordered payload notification

Hot-path note

More structure than plain events; useful for phases.

AzPriorityEventRef<T>

Normal use

Ordered large struct payload

Hot-path note

For hot ordered paths with struct payloads.

AzState<T>

Normal use

Current meaningful value

Hot-path note

Prefer for values that need initial delivery and later changes.

Invoke Variants

Use the safest, clearest invocation that fits the path.

Invoke

Best for

Normal gameplay dispatch for events, priority events, and ref events

Tradeoff

Balanced default.

InvokeSafe

Best for

Dispatch where one bad listener should not break others

Tradeoff

Exception isolation costs more than trusted direct dispatch.

InvokeFast

Best for

Measured AzPriorityEvent hot paths with trusted listeners

Tradeoff

Priority-event fast path with fewer protections.

Example:

using Azkar.Eda;

public static class FrameEvents
{
    public static readonly AzEvent<int> FrameSampled = new();
}
using UnityEngine;

public sealed class FrameSampler : MonoBehaviour
{
    private void Update()
    {
        FrameEvents.FrameSampled.Invoke(Time.frameCount);
    }
}

Do not start with InvokeFast. First make the lifecycle correct, then profile, then use priority-event fast invocation only on handlers you trust. Plain AzEvent code should normally choose between Invoke and InvokeSafe.

SourceSet, Scheduling, And Coalescing

AzState<T> is for values with current meaning. In state-heavy systems, SourceSet gives you distinct-until-changed writes and participates in the state's scheduling behavior.

using UnityEngine;
using Azkar.Eda;

public sealed class HealthWriter : MonoBehaviour
{
    public void ApplyDamage(int amount)
    {
        int next = PlayerState.Health.Value - amount;
        PlayerState.Health.SourceSet(next);
    }
}

State guidance:

  • Use state when late subscribers need the current value.
  • Prefer one owner or a clear set of allowed writers.
  • Keep state ownership clear so you can debug who changed a value.
  • Expect scheduled state writes to notify according to their scheduling rules, not necessarily at the exact line where the write was requested.
  • Avoid writing state in tight loops if you only need the final value. Coalesce and publish once.

Main Thread Rules

Unity has a practical rule: most Unity APIs belong on the main thread.

Keep these on the main thread:

  • UnityEngine.Object access.
  • Scene object creation and destruction.
  • Transform changes.
  • UI updates.
  • Most event invocations that reach scene objects.
  • Most state writes that update UI or scene listeners.

Move plain data across thread boundaries, then publish on the main thread.

using System.Collections.Concurrent;
using UnityEngine;
using Azkar.Eda;

public sealed class MainThreadBridge : MonoBehaviour
{
    private readonly ConcurrentQueue<float> _damageResults = new();

    public void EnqueueDamageFromWorker(float amount)
    {
        _damageResults.Enqueue(amount);
    }

    private void Update()
    {
        while (_damageResults.TryDequeue(out float amount))
        {
            CombatEvents.DamageApplied.Invoke(amount);
        }
    }
}

Off Thread Caveats

When working off thread:

  • Use plain data only.
  • Avoid captured MonoBehaviour, GameObject, Transform, ScriptableObject, or UI references.
  • Do not assume a handler is thread-safe because the event type can be referenced from a worker.
  • Be careful with subscription and disposal while another thread is publishing.
  • Prefer queueing results back to a main-thread bridge.

Token Lifecycle Cost

Tokens and bags make cleanup explicit. They also make lifecycle choices visible.

Fast and clear:

using UnityEngine;
using Azkar.Eda;

public sealed class MenuView : MonoBehaviour
{
    private readonly AzEventTokenBag _tokens = new();

    private void OnEnable()
    {
        MenuEvents.Opened.Subscribe(OnOpened).AddTo(_tokens);
    }

    private void OnDisable()
    {
        _tokens.DisposeAll();
    }

    private void OnOpened() { }
}

Guidance:

  • Subscribe once per owner lifecycle.
  • Dispose the bag at the matching lifecycle boundary.
  • Prefer pausing and resuming only when you need temporary silence without losing ownership.
  • Dispose when the listener is no longer valid.
  • Avoid subscribing every frame.
  • Avoid creating short-lived lambdas in hot loops.
  • Batch related subscriptions into one bag.
  • Trim capacity only after large one-time subscription spikes if the guide for that type supports it.
  • Use metrics and profiling before optimizing token operations.

Tracking Cost

Tracking is an editor observability tool. It is valuable while debugging, but captured value samples and high-volume activity can add overhead.

Guidance:

  • Enable CaptureValues where supported value samples help debugging.
  • Avoid capturing noisy payloads on extremely hot events unless needed.
  • Use groups and tags so filters stay usable.
  • Turn off unnecessary runtime registrations when a temporary debugging session is done.

Practical Performance Checklist

  • Use AzEvent for unordered signals.
  • Use AzPriorityEvent only when order matters.
  • Use AzState<T> for current values.
  • Keep subscription setup outside tight loops.
  • Dispose bags reliably.
  • Use ref payloads for large struct payloads.
  • Coalesce repeated state writes when only the final value matters.
  • Queue worker results back to main thread.
  • Profile before changing priority-event Invoke calls to InvokeFast.