Skip to content

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 panel
game.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.

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.

Simulation.run builds a self-contained load world and returns a handle whose stop() removes every trace:

  • Virtual sessions: Entities fake players (Sim_1, Sim_2 and 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 Fire or FireClient that 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 the ChloeKernelSimulation folder in the Workspace that holds the visuals.

A load mode runs ten rigs, each a real subsystem under synthetic pressure:

  1. Entity movement: one Process per virtual player.
  2. Effects churn across three stacking buff definitions.
  3. 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.
  4. A Bus flood on SimTelemetry.Tick with an exact and a wildcard (SimTelemetry.*) subscriber.
  5. Replicas doing real interest scans and struct delta encoding.
  6. Serde round trips through a typed schema codec.
  7. Zones sweeps and diffs over the virtual positions, with an injected query.
  8. Visible projectile arcs on Process-driven ballistics.
  9. Pooled part churn.
  10. 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=900KB

Showcase 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.

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.

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 SimControl intent.
  • Client load (this machine): runs Debug/SimLoad locally. 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.

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 as Process SimEntity and Process SimBolt (client loads: Process SimLoadEntity0 to SimLoadEntity3 and Process 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 before Deferred goes above zero. Under HighIntensity on a modern server it sits low and green; under Overload it pegs at 100%.
  • Deferred is green only at 0. Overload forces 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), gcCycles and gcReclaim. Compare them against the 10-second heartbeat to confirm the load is real.
  • The NET window during client loads shows SimNoise traffic with about 10 RateLimited verdicts 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.

  • Run SmallGame first and note the step time and GC numbers: that is what a light game costs on this hardware. The HighIntensity difference, divided by the rate differences, tells you roughly what each kind of operation costs at scale.
  • HighIntensity reading 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 under HighIntensity is 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.
  • Overload checks how the kernel degrades. It says nothing about capacity. Confirm deferral instead of frame death, Background starving before Low (the burn alternates priorities to make the order visible), Kernel.Overload firing, and a clean recovery when you switch the mode off. Never tune a game to survive Overload; it is built to be unsurvivable.
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().

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)
  • Production is gated three ways. The entry script needs Studio or ChloeKernelDebug = true; both intents check NetTap.debugAllowed, which honors ChloeKernelDebugUserIds, 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 SkipLoop plus manual sweeps so the harness owns their cadence. Specs use the same trick to make time deterministic.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.