Skip to content

Performance Tuning

iKryptonic edited this page May 1, 2026 · 3 revisions

Performance Tuning

How to tune scheduler cost, FSM cadence, entity replication, and dashboard-driven profiling in the current runtime.

Important

This page is based on Core/Settings.luau, Core/Scheduler/*, BaseStateMachine.luau, Factory/init.luau, and ServiceManager profiling/insight modules.


1. Measure first, then tune

RBXStateMachine already ships with multiple performance surfaces:

  • Scheduler.PerformanceManager:GetStatsSnapshot()
  • Scheduler:GetThreadPoolStats()
  • ServiceManager TASKS
  • ServiceManager PROFILER
  • ServiceManager INSIGHTS
  • per-FSM Perf fields

Use those before changing priorities, budgets, or replication settings.


2. Frame budget: the scheduler's top-level limit

The current repo settings default to:

Scheduler = {
    FrameBudget = 0.015,
}

That means the shared scheduler is allowed to spend about 15 ms per frame executing scheduled work.

Important nuance

Scheduler.new() has an internal fallback of 0.005 if no initialized settings are available, but the normal Orchestrator bootstrap path initializes the scheduler from Core/Settings.luau, so the practical default for this repo is 15 ms.

How budget exhaustion behaves

When the scheduler runs out of frame budget:

  • it stops executing additional tasks for the current frame
  • remaining tasks stay in the heap
  • work resumes on the next frame

Tuning advice

Budget Use when Tradeoff
0.005 heavy experiences with lots of non-framework work safest for engine headroom, lowest scheduler throughput
0.010 most active multiplayer servers good compromise
0.015 default repo behavior / moderate workloads higher throughput, less headroom
0.020+ only after real profiling proves safe easiest way to hide hot code without fixing it

3. FSM priority controls cadence, not importance labels

BaseStateMachine.Priorities currently defines:

  • Render = 1
  • High = 2
  • Medium = 5
  • Low = 10
  • Background = 30

Lower numeric values update more often.

Practical cadence at ~60 FPS

Priority Approx cadence Good for
Render every frame local-player critical behavior, render-coupled effects
High every 2 frames responsive combat logic
Medium every 5 frames general AI
Low every 10 frames ambient world logic
Background every 30 frames low-urgency timers, distant simulation

Important behavior

Skipped time is accumulated. A background FSM does not “lose” time; it receives a larger dt when its turn arrives.

Recommended mapping

  • player-local responsivenessRender
  • authoritative combat AIHigh
  • standard NPCsMedium
  • ambient/world simulationLow or Background

Caution

Do not use Render as the default for everything. It is the easiest way to create silent scale problems.


4. Avoid expensive work in the hot path

The hottest paths are usually:

  • render/high-priority FSM OnHeartbeat
  • recurring scheduler tasks
  • frequent entity commits with replication
  • dashboard-heavy development sessions with lots of live telemetry

High-impact fixes

  • move heavy one-shot setup into OnEnter
  • keep OnHeartbeat small and branch-light
  • avoid repeated WaitForChild / expensive hierarchy scans per update
  • cache runtime references in GetContext()
  • use ScheduleTransition() instead of custom timer closures
  • push low-urgency work into lower-priority FSMs or recurring scheduler tasks

Especially important for Render

Render-priority FSMs run inline through BaseStateMachine.Step(dt). Yields or expensive work there have immediate user-facing cost.


5. Use entity replication deliberately

Server-side entity updates replicate only fields marked:

Replicate = true

Anything else stays server-local.

First tuning lever: replicate fewer fields

Only replicate values clients actually need.

Good replicated fields:

  • Health
  • IsOpen
  • CurrentAnimation
  • Tint

Poor replicated fields:

  • server-only scratch tables
  • temporary pathfinding intermediates
  • references the client can derive locally

Second tuning lever: Replication.RateLimit

In the current factory implementation, rate-limited entity replication uses:

local interval = 1 / entityRateLimit

That means RateLimit is effectively updates per second, not “seconds between updates”.

Example:

Replication = {
    Enabled = true,
    RateLimit = 20,
}

means “send at most about 20 packets per second for this entity”.

Third tuning lever: batch semantics

When rate limiting is active, replicated changes are buffered into ReplicationState.Pending and sent together when the interval opens.

That means frequent small updates to the same entity can collapse into fewer packets.


6. Pool bursty entities instead of thrashing allocations

The factory and registry support entity pooling. Pooling is most useful when you create and destroy many similar transient entities:

  • bullets
  • hitboxes
  • temporary interactables
  • effect controllers

Benefits:

  • lower allocation churn
  • fewer GC spikes
  • less constructor/setup overhead during bursts

Use pooling when object lifetime is short and class shape is stable.


7. Read the actual performance surfaces

7.1 Scheduler snapshot

local stats = Orchestrator.Scheduler.PerformanceManager:GetStatsSnapshot()
for name, info in pairs(stats) do
    print(name, info.Runs, info.Avg, info.Max)
end

Use it to find:

  • tasks with high Avg
  • tasks with spiky Max
  • tasks running far more often than expected

7.2 Thread pool stats

local threads = Orchestrator.Scheduler:GetThreadPoolStats()
print(threads.TotalThreads, threads.PeakThreads, threads.ActiveThreads)

Use this when scheduled work seems to create bursty async pressure.

7.3 ServiceManager TASKS

Best for verifying:

  • GlobalStateMachineHeartbeat exists
  • per-task counts and timing
  • whether a recurring task is unexpectedly hot

7.4 ServiceManager PROFILER

Best for:

  • overall budget visibility
  • top hot tasks
  • top hot FSMs
  • memory/FPS trend checks

7.5 ServiceManager INSIGHTS

Best for anomaly hints, such as:

  • too many render-priority FSMs
  • stalled state machines
  • FPS degradation
  • memory pressure

8. Tuning patterns by scenario

Scenario A: many NPCs, low urgency

  • move patrol/ambient AI to Medium or Low
  • keep combat escalation machines separate and higher priority
  • replicate only visible/player-relevant state

Scenario B: high-frequency cosmetic updates

  • rate-limit replication
  • move purely visual interpolation to the client
  • send target state, not every intermediate step

Scenario C: spikes during match start

  • pre-create or pool bursty entities
  • split heavy initialization into staged jobs
  • avoid creating many Render FSMs simultaneously if Medium would do

Scenario D: dashboard shows frozen FSMs under load

  • check whether budget starvation is delaying the global heartbeat task
  • lower work per tick before raising the frame budget
  • inspect Max timings for burst tasks

9. Common performance mistakes

Mistake: using Render as the default priority

This usually hides design indecision and creates avoidable scheduler pressure.

Mistake: replicating rapidly changing values every frame

Use client interpolation or coarse-grained replicated targets instead.

Mistake: doing setup work in every heartbeat

Cache and precompute in GetContext() or OnEnter.

Mistake: tuning budget before measuring real hot spots

FrameBudget should be the last lever, not the first.

Mistake: ignoring the dashboard because Studio “feels fine”

Hot paths frequently show up only when many machines coexist.


10. Practical checklist

  • priorities chosen intentionally
  • replicated fields minimized
  • rate limits applied where high-frequency updates occur
  • expensive setup moved out of OnHeartbeat
  • pooled classes identified for burst workloads
  • TASKS, PROFILER, or GetStatsSnapshot() inspected before tuning
  • frame budget changed only with evidence

Related guides

Clone this wiki locally