-
Notifications
You must be signed in to change notification settings - Fork 0
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.
RBXStateMachine already ships with multiple performance surfaces:
Scheduler.PerformanceManager:GetStatsSnapshot()Scheduler:GetThreadPoolStats()- ServiceManager
TASKS - ServiceManager
PROFILER - ServiceManager
INSIGHTS - per-FSM
Perffields
Use those before changing priorities, budgets, or replication settings.
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.
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.
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
| 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 |
BaseStateMachine.Priorities currently defines:
Render = 1High = 2Medium = 5Low = 10Background = 30
Lower numeric values update more often.
| 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 |
Skipped time is accumulated. A background FSM does not “lose” time; it receives a larger dt when its turn arrives.
-
player-local responsiveness →
Render -
authoritative combat AI →
High -
standard NPCs →
Medium -
ambient/world simulation →
LoworBackground
Caution
Do not use Render as the default for everything. It is the easiest way to create silent scale problems.
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
- move heavy one-shot setup into
OnEnter - keep
OnHeartbeatsmall 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
Render-priority FSMs run inline through BaseStateMachine.Step(dt). Yields or expensive work there have immediate user-facing cost.
Server-side entity updates replicate only fields marked:
Replicate = trueAnything else stays server-local.
Only replicate values clients actually need.
Good replicated fields:
HealthIsOpenCurrentAnimationTint
Poor replicated fields:
- server-only scratch tables
- temporary pathfinding intermediates
- references the client can derive locally
In the current factory implementation, rate-limited entity replication uses:
local interval = 1 / entityRateLimitThat 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”.
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.
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.
local stats = Orchestrator.Scheduler.PerformanceManager:GetStatsSnapshot()
for name, info in pairs(stats) do
print(name, info.Runs, info.Avg, info.Max)
endUse it to find:
- tasks with high
Avg - tasks with spiky
Max - tasks running far more often than expected
local threads = Orchestrator.Scheduler:GetThreadPoolStats()
print(threads.TotalThreads, threads.PeakThreads, threads.ActiveThreads)Use this when scheduled work seems to create bursty async pressure.
Best for verifying:
-
GlobalStateMachineHeartbeatexists - per-task counts and timing
- whether a recurring task is unexpectedly hot
Best for:
- overall budget visibility
- top hot tasks
- top hot FSMs
- memory/FPS trend checks
Best for anomaly hints, such as:
- too many render-priority FSMs
- stalled state machines
- FPS degradation
- memory pressure
- move patrol/ambient AI to
MediumorLow - keep combat escalation machines separate and higher priority
- replicate only visible/player-relevant state
- rate-limit replication
- move purely visual interpolation to the client
- send target state, not every intermediate step
- pre-create or pool bursty entities
- split heavy initialization into staged jobs
- avoid creating many
RenderFSMs simultaneously ifMediumwould do
- check whether budget starvation is delaying the global heartbeat task
- lower work per tick before raising the frame budget
- inspect
Maxtimings for burst tasks
This usually hides design indecision and creates avoidable scheduler pressure.
Use client interpolation or coarse-grained replicated targets instead.
Cache and precompute in GetContext() or OnEnter.
FrameBudget should be the last lever, not the first.
Hot paths frequently show up only when many machines coexist.
- 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, orGetStatsSnapshot()inspected before tuning - frame budget changed only with evidence
Quick Links: Home · Quick Start · API Reference · Architecture · Examples · Glossary
Copyright: © 2026 RBXStateMachine contributors · Repository · License information