Simulation
The built-in load harness. Synthetic rigs drive real kernel code paths at game-shaped rates so the debug panel shows where a server strains before a game does.
- Realm
- Server + Client
- Require
ServerScriptService.ChloeKernelServer .Simulation - Since
- v0.3.0
- Source
- Simulation/init.luauServer/Simulation.server.luauDebug/SimLoad.luauDebug/SimPanel.luau
Simulation is the kernel’s load harness. Synthetic rigs drive real kernel code paths at game-shaped rates, so the debug panels and the hot-task profiler show where a server strains before your game does. Set one attribute and press Play. The rule that defines it: fake wire, real subsystems. Simulated channels use fake packet factories, so a connected client’s real Packet stream never carries simulation traffic, while everything else (the Scheduler, Processes, Effects, Zones, replica interest scans and deltas, hook chains, Bus fan-out, pools) is the real subsystem under real load.
-- In the Studio command bar during Play, or on ReplicatedStorage in the Properties panelgame.ReplicatedStorage:SetAttribute("ChloeKernelSimulation", "HighIntensity")The ChloeKernelSimulation attribute on ReplicatedStorage takes three load modes and a narrated tour. An empty string
stops simulation. Changing the value mid-Play swaps modes: the old mode tears down completely before the new one starts.
| Attribute value | Shape | What it shows |
|---|---|---|
HighIntensity |
A busy shooter-scale server | Realistic full load. A modern server clears it in about 1 to 2 ms per frame and reads green under frame-aware budgeting. That is the point. |
SmallGame |
A social or obby-scale baseline | The same rigs at about a tenth of the rates, with no CPU burn. The comparison anchor. |
Overload |
A deliberate torture test | About 20 ms of scheduled demand against a frame target of about 16.5 ms. The budget pegs, Deferred goes above zero, and the panel’s red states show. Not a model of anything real. |
Showcase |
A narrated feature tour | Six acts, one every 8 seconds, narrated in the Output window and the panel’s log tail. |
Load mode numbers
Section titled “Load mode numbers”Each load mode is a ModeConfig in Simulation.Modes:
| Knob | HighIntensity |
SmallGame |
Overload |
Drives |
|---|---|---|---|---|
Entities |
200 | 20 | 500 | Virtual players, one kernel Process each. |
ProjectilesPerSecond |
30 | 3 | 60 | Visible arcs simulated by Processes. |
EffectOpsPerSecond |
60 | 6 | 120 | Effects apply and expire churn across virtual sessions. |
BusMessagesPerSecond |
500 | 50 | 1000 | Bus publishes against exact and wildcard subscribers. |
HookFiresPerSecond |
300 | 30 | 600 | Fail-closed hook chains of three validators. |
Replicas |
8 | 2 | 16 | Delta-encoding replicas on a fake wire. |
ReplicaViewers |
30 | 8 | 60 | Fake subscriber count for interest scans. |
ReplicaWritesPerSecond |
120 | 10 | 240 | Field writes producing struct deltas. |
SerdeOpsPerSecond |
100 | 10 | 200 | Profile-sized Serde encode and decode round trips. |
PartChurnPerSecond |
20 | 4 | 20 | Pool part acquire and release. |
BurnMsPerTick |
2 | 0 | 2.5 | A deliberate CPU busy-wait per burn task per interval. |
BurnTasks |
1 (default) | none | 8 | Parallel burn tasks, alternating Low and Background priority. |
BurnIntervalSeconds |
0.1 (default) | none | 0 (every step) | Burn cadence. |
Overload’s arithmetic: 8 tasks x 2.5 ms, every step, is about 20 ms of demand against a frame target of about 16.5 ms, so the Scheduler must defer work on every frame. Several mid-size tasks instead of one huge one keep the deferral counts readable.
Mental model
Section titled “Mental model”Simulation.run builds a self-contained load world and returns a handle whose stop() removes every trace:
- Virtual sessions:
Entitiesfake players (Sim_1,Sim_2and so on, with negative UserIds) that have positions but no Instances. A random-walk Process per entity moves them. - A sim kernel facade. The attached subsystems (Effects, Zones, the replica service) receive a wrapper that exposes the real Bus, Hooks, Scheduler and priorities, but a session map holding only the virtual sessions, so simulation churn never touches real players’ state.
- Fake packet factories. Replica traffic goes through the real struct-delta encoder, then reaches a
FireorFireClientthat does nothing. - A process tracker. Every spawned Process registers itself.
stop()kills any still running, runs every cleanup in reverse order (cancelling recurring jobs, detaching subsystems, disconnecting subscriptions), and destroys theChloeKernelSimulationfolder in the Workspace that holds the visuals.
A load mode runs ten rigs, each a real subsystem under synthetic pressure:
- Entity movement: one Process per virtual player.
- Effects churn across three stacking buff definitions.
- A hook flood through
Sim.Intent, a fail-closed chain of three validators: a clamp at priority 10, a gate at 20 and an observer at 30. - A Bus flood on
SimTelemetry.Tickwith an exact and a wildcard (SimTelemetry.*) subscriber. - Replicas doing real interest scans and struct delta encoding.
- Serde round trips through a typed schema codec.
- Zones sweeps and diffs over the virtual positions, with an injected query.
- Visible projectile arcs on Process-driven ballistics.
- Pooled part churn.
- The CPU burn.
The Effects and Zones rigs attach with SkipLoop = true and run their sweeps from recurring jobs the harness owns, so
stop() cancels them like everything else. Every 10 seconds a GcWatch heartbeat logs the achieved
rates and the collector cost:
[Simulation] entities=200 hooks/s=300 bus/s=500 effects/s=60 serde/s=100 bolts=214 inZone=12 gcAlloc=3800KB/s gcCycles=25.2/min gcReclaim=900KBShowcase is different in kind: it runs against the real kernel. Its six acts, one every 8 seconds, are eight orbiting
Processes, haste then slow on every player present (real Effects, so the WalkSpeed change is felt), a fan of 12
projectile arcs, full-snapshot and one-field delta byte counts for a replica struct, a trade replayed through
Transactions to show the replay rejection, and Serde bytes against JSON bytes. Each act is
narrated in the log by name, for example [Simulation] [1/6] Processes: 8 orbiting parts, one kernel Process each.
Running it
Section titled “Running it”The attribute, the entry script and the gate
Section titled “The attribute, the entry script and the gate”Simulation.server.luau owns the wiring. It runs only in Studio, or on a server whose ReplicatedStorage has
ChloeKernelDebug = true; anywhere else it returns before defining anything. It then waits for the kernel that Main
boots: ServerKernel.current() is retried each time ReplicatedStorage’s ChloeKernelServer attribute changes (kernel
start sets it to the version, shutdown clears it), with no per-frame polling. Once the kernel is there, it applies the
current ChloeKernelSimulation value and again on every change. A mode name that Simulation.run rejects is warned and
ignored.
For live servers it defines two real kernel channels:
| Channel | Kind | Schema | Limits | Purpose |
|---|---|---|---|---|
SimControl |
Intent | { "String" } |
RateLimit = 5 |
Sets the simulation attribute remotely. The validator allows only players NetTap.debugAllowed accepts, and only known mode names; "Off" maps to the empty string. |
SimNoise |
Intent | { "NumberU16" } |
RateLimit = 50, Burst = 10 |
A sink for client-side load traffic, validated with NetTap.debugAllowed. The 50-per-second limit sits below the client rig’s 60 per second on purpose: the roughly 10 RateLimited verdicts per second in the NET window prove the rate limit fires. |
NetTap.debugAllowed is true in Studio. On a live server it needs ChloeKernelDebug = true and then allows the
operators in NetTap.OwnerUserIds (CryptedChloes, the framework’s only developer, plus any a game adds) and the players
listed in the ChloeKernelDebugUserIds attribute (a comma-separated list, or * for everyone on a test place). With
the list unset it fails closed to those operators. See
Production access control. Both channels are ordinary fail-closed
intents, so driving the harness exercises the full validation pipeline and shows up in the NET window like any other
channel.
The SIM CONTROL window
Section titled “The SIM CONTROL window”Debug/SimPanel, attached from the client Bootstrap, gives you buttons instead of attributes. It uses the same F8
toggle and NetTap.debugAllowed gate as the debug panels, starts hidden, and renders every
0.15 s only while it is shown. Its two sections start and stop independently:
- Server load: buttons for the four server modes, sent through the
SimControlintent. - Client load (this machine): runs
Debug/SimLoadlocally. SimLoad builds the same kind of rigs on shared primitives only (Processes, the Scheduler, the Bus, hooks, Serde, pools), because the client has no richer Simulation module.
| SimLoad knob | HighIntensity |
SmallGame |
|---|---|---|
Entities |
150 | 15 |
BoltsPerSecond |
20 | 3 |
BusMessagesPerSecond |
400 | 40 |
HookFiresPerSecond |
240 | 24 |
SerdeOpsPerSecond |
80 | 8 |
BurnMsPerTick |
2 | 0 |
IntentsPerSecond |
60 | 5 |
IntentsPerSecond is the one client rig that touches the real wire: SimNoise intents through the real network path,
deliberately over the rate limit in HighIntensity. Client bolts render green and server bolts orange, so you can tell
whose load you are watching. Load both machines at once and watch the CLIENT and SERVER windows react separately.
SimPanel.attach(kernel) returns false when the player is not allowed, and otherwise a handle whose detach()
disconnects the toggle, stops rendering, stops a running client load and destroys the window.
What to watch while it runs
Section titled “What to watch while it runs”Open the panel with F8 and read these, most useful first:
- Hot tasks name the harness’s own origins. The burn shows as its defining line,
Simulation:<line>, and the rigs asProcess SimEntityandProcess SimBolt(client loads:Process SimLoadEntity0toSimLoadEntity3andProcess SimLoadBolt). If hot-task attribution can name the harness’s lines, it can name your game’s. - The frame budget bar (
used / budget ms) shows how close the kernel is to deferring beforeDeferredgoes above zero. UnderHighIntensityon a modern server it sits low and green; underOverloadit pegs at 100%. Deferredis green only at 0.Overloadforces it above zero on every step; anything else doing that is a finding.- The GC rows show the collector cost of the burn and churn rigs:
gcAlloc(the allocation rate, the lever),gcCyclesandgcReclaim. Compare them against the 10-second heartbeat to confirm the load is real. - The NET window during client loads shows
SimNoisetraffic with about 10RateLimitedverdicts per second.
The same numbers are available in code:
local Stats = Kernel:stats()print(Stats.Scheduler.LastStepSeconds, Stats.Scheduler.TasksRun, Stats.Scheduler.TasksDeferred)Kernel:enableDiagnostics(intervalSeconds?) publishes the server’s snapshot as attributes for the panel, and raises the
Kernel.Overload Bus topic after three diagnostic windows in a row with deferred work, which Overload triggers
reliably.
Interpreting results
Section titled “Interpreting results”- Run
SmallGamefirst and note the step time and GC numbers: that is what a light game costs on this hardware. TheHighIntensitydifference, divided by the rate differences, tells you roughly what each kind of operation costs at scale. HighIntensityreading green is the pass condition. It is a realistic busy-shooter profile, and a server that clears it has headroom for a real game. The steady-state kernel step time underHighIntensityis the kernel’s share of a busy frame; the rest of the 16.5 ms belongs to your game logic, physics and the engine.- Compare your game with the mode it resembles. If your place’s rates (Bus messages, hook fires, replica writes per
second) exceed
HighIntensity’s, those rows are your budget lines. Use hot tasks to attribute the cost before you cut rates. Overloadchecks how the kernel degrades. It says nothing about capacity. Confirm deferral instead of frame death,Backgroundstarving beforeLow(the burn alternates priorities to make the order visible),Kernel.Overloadfiring, and a clean recovery when you switch the mode off. Never tune a game to surviveOverload; it is built to be unsurvivable.
API reference
Section titled “API reference”| Member | Description |
|---|---|
Simulation.run(kernel, modeName) -> handle |
Starts "HighIntensity", "Overload", "SmallGame" or "Showcase". Errors on any other name. |
handle.stop() |
Kills tracked Processes, runs every cleanup in reverse order and destroys the visuals folder. Idempotent. |
Simulation.Modes |
The ModeConfig table per load mode. Read it to derive rates, or to compare your own game’s numbers. |
SimLoad.run(kernel, modeName) -> handle |
Shared-primitive load on either side, "HighIntensity" or "SmallGame", with the same handle. Lives at ReplicatedStorage.ChloeKernel.Debug.SimLoad. |
SimPanel.attach(kernel) -> handle or false |
The SIM CONTROL window. false when NetTap.debugAllowed refuses the local player; otherwise a handle with detach(). |
Names the harness owns
Section titled “Names the harness owns”The harness’s own traffic is namespaced so it never collides with game channels or topics:
| Name | Kind | Owner |
|---|---|---|
Sim.Intent |
Hook point with three validators | Server load rigs |
SimTelemetry.Tick, SimTelemetry.* |
Bus topic and wildcard subscriber | Server load rigs |
SimLoad.Intent, SimLoad.Tick, SimLoad.* |
Hook point and Bus topics | SimLoad rigs |
SimControl, SimNoise |
Wire intents | Simulation.server.luau |
ChloeKernelSimulation |
Workspace folder | Load mode and Showcase visuals (ChloeKernelSimLoad<Role> for SimLoad) |
Gotchas and design notes
Section titled “Gotchas and design notes”- Production is gated three ways. The entry script needs Studio or
ChloeKernelDebug = true; both intents checkNetTap.debugAllowed, which honorsChloeKernelDebugUserIds, so flagging a live server does not hand load controls to every player; and with no list set the surface fails closed to owners. - Teardown is total by construction. Swapping modes or clearing the attribute kills tracked Processes, cancels every recurring job, disconnects subscriptions, detaches the attached subsystems and destroys the folder. If you build your own rigs on this pattern, copy the reverse-order cleanup list.
- The Effects and Zones rigs use
SkipLoopplus manual sweeps so the harness owns their cadence. Specs use the same trick to make time deterministic.