Simulation
Simulation is the kernel’s built-in load harness: synthetic rigs drive real kernel codepaths at game-shaped intensities, so the debug panel and hot-task profiler show exactly where things strain before your game does. Set one attribute and press Play. The defining design decision: fake wire, real subsystems. Simulated channels ride fake packet factories — a connected client’s real Packet stream can never be corrupted by simulation traffic — while everything else (scheduler, processes, effects sweeps, zone diffs, replica interest scans and struct deltas, hook chains, bus fan-out, pools) is the genuine subsystem under genuine load.
-- in the command bar (or the Properties panel on ReplicatedStorage):game.ReplicatedStorage:SetAttribute("ChloeKernelSimulation", "HighIntensity")The ChloeKernelSimulation attribute takes four values — three load modes plus a narrated tour. An empty string stops simulation; changing the value mid-Play hot-swaps modes (the old mode tears down completely before the new one starts).
| Attribute value | Shape | What it shows |
|---|---|---|
HighIntensity |
busy shooter-scale server | Realistic full load. A modern server clears it in ~1–2ms/frame — it reads green under frame-aware budgeting, and that is the point |
SmallGame |
social/obby-scale baseline | The same rigs at ~1/10 intensity, no CPU burn — the comparison anchor |
Overload |
deliberate torture test | ~20ms of scheduled demand against a ~16.5ms frame target — the budget pegs, Deferred goes nonzero, the red panel states show. Not a model of anything real |
Showcase |
narrated feature tour | Cycles 6 acts every 8s with narration in the console and the panel log tail |
Load mode numbers
Section titled “Load mode numbers”Every load mode is a ModeConfig — the exact rates each rig runs at:
| Knob | HighIntensity |
SmallGame |
Overload |
Drives |
|---|---|---|---|---|
Entities |
200 | 20 | 500 | Virtual players, one kernel Process each |
ProjectilesPerSecond |
30 | 3 | 60 | Visible process-simulated arcs |
EffectOpsPerSecond |
60 | 6 | 120 | Effects apply/expire churn across virtual sessions |
BusMessagesPerSecond |
500 | 50 | 1000 | Bus publishes against exact + wildcard subscribers |
HookFiresPerSecond |
300 | 30 | 600 | Real 3-validator fail-closed hook chains |
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+decode round trips |
PartChurnPerSecond |
20 | 4 | 20 | Pooled part acquire/release |
BurnMsPerTick |
2 | 0 | 2.5 | Deliberate CPU busy-wait per burn task per interval |
BurnTasks |
1 (default) | — | 8 | Parallel burn tasks, alternating Low/Background priorities |
BurnIntervalSeconds |
0.1 (default) | — | 0 (every step) | Burn cadence |
Overload’s arithmetic: 8 tasks × 2.5ms, every step — ~20ms of demand against the ~16.5ms frame target, so the Scheduler must defer work every single frame. Several mid-size tasks instead of one huge one keep the deferral counts legible.
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_2, … with negative UserIds) with positions but no Instances. A random-walk Process per entity moves them. - A sim-kernel facade. Attached subsystems (Effects, Zones, Replicas) receive a wrapper exposing the real Bus, Hooks, Scheduler, and Priority — but a session map containing only the virtual sessions, so simulation churn never touches real players’ state.
- Fake packet factories. Replica traffic serializes through the real struct-delta encoder, then hits a
Fire/FireClientno-op instead of the wire. - A process tracker. Every spawned Process registers itself;
stop()kills any still running, cancels every recurring task in reverse order, and destroys the workspace folder (ChloeKernelSimulation) holding the visuals.
The ten rigs a load mode runs, each a real subsystem under synthetic pressure: entity-movement Processes; Effects churn across three stacking buff definitions; a 3-validator fail-closed hook chain flood (Sim.Intent: a clamp, a gate, an observer); a bus flood with exact and wildcard subscribers; replicas doing real interest scans and struct delta encoding; typed-codec Serde round trips; Zones sweep/diff over the virtual positions with an injected query; visible projectile arcs on process-driven ballistics; pooled part churn; and the CPU burn. A GcWatch heartbeat logs achieved rates and collector cost every 10 seconds:
[Simulation] entities=200 hooks/s=300 bus/s=500 effects/s=60 serde/s=100 bolts=214 inZone=12 gcAlloc=3800KB/s gcCycles=252/min gcReclaim=900KBThe Showcase mode is different in kind: it runs against the real kernel (its haste/slow act applies genuine Effects to present players — you feel the WalkSpeed change), cycling six acts every 8 seconds: orbiting Processes, effects on players, a 12-arc projectile fan, replica delta byte counts, an anti-dupe trade replay rejection through Transactions, and Serde-vs-JSON byte comparisons — each narrated into the log tail.
Running it
Section titled “Running it”The attribute, the entry script, the gate
Section titled “The attribute, the entry script, the gate”Simulation.server.luau owns the wiring: it waits for the booted kernel (Main owns boot), applies whatever ChloeKernelSimulation holds, and re-applies on every attribute change. It is Studio-only unless ChloeKernelDebug is flagged — in production without the flag the script returns before defining anything, so the harness is inert.
For live-server use it defines two real kernel channels:
| Channel | Kind | Schema | Config | Purpose |
|---|---|---|---|---|
SimControl |
Intent | { "String" } |
RateLimit = 5 |
Sets the simulation attribute remotely. The validator allowlists callers via NetTap.debugAllowed — Studio, or flagged servers restricted to ChloeKernelDebugUserIds — and whitelists mode names ("Off" maps to the empty string) |
SimNoise |
Intent | { "NumberU16" } |
RateLimit = 50, Burst = 10 |
A sink for client-side load traffic. The 50/s limit sits below the client rig’s 60/s HighIntensity flood on purpose: the ~10/s of RateLimited verdicts in the NET panel prove the rate limiting gate fires |
Because SimControl is an ordinary fail-closed intent, driving the harness exercises the full validation pipeline — and shows up in the NET panel like any other channel.
The SIM CONTROL panel
Section titled “The SIM CONTROL panel”Debug/SimPanel (wired through this place’s client Bootstrap; same F8 toggle and NetTap.debugAllowed gate as the debug panel) gives you buttons instead of attributes. Its two sections start and stop independently:
- Server load — buttons for all four server modes, fired through the
SimControlintent. - Client load (this machine) — runs
Debug/SimLoadlocally: the same rig style built on shared primitives only (processes, scheduler, bus, hooks, serde, pools — no server subsystems), because the client has no richer Simulation module. ModesHighIntensity(150 entities, 20 bolts/s, 400 bus msgs/s, 240 hook fires/s, 80 serde ops/s, 2ms burn, 60 intents/s) andSmallGame(~1/10, no burn, 5 intents/s). Client bolts render green, server bolts orange, so you can tell whose load you are watching.
SimLoad’s IntentsPerSecond is the one client-only rig that touches the real wire — SimNoise intents through the genuine net path, deliberately exceeding the rate limit as described above. Stress both machines at once and watch the CLIENT and SERVER panel windows react separately.
What to watch while it runs
Section titled “What to watch while it runs”Open the panel (F8) and read these, in order of usefulness:
- Hot tasks name the simulation’s own origins. The burn shows as its defining
Simulation:<line>, the rigs asProcess SimEntity/Process SimBolt(client loads:SimLoadEntity/SimLoadBolt). If hot-task attribution can name the harness’s exact lines, it can name your game’s. - The frame budget bar (
used / budget ms) — how close the kernel is to deferring, beforeDeferredgoes nonzero. Under HighIntensity on a modern server it sits low and green; under Overload it pegs at 100%. Deferred— green only at 0. Overload forces it nonzero every step; anything else driving it nonzero is a finding.- GC rows — the burn and churn rigs have a measurable collector cost:
gcAlloc(allocation rate — the lever),gcCycles,gcReclaim. Compare against the 10s heartbeat’s logged numbers to confirm the load is real. - The NET window during client loads —
SimNoisetraffic withRateLimitedverdicts at ~10/s.
The same numbers are available programmatically:
local Stats = Kernel:stats()print(Stats.Scheduler.LastStepSeconds, Stats.Scheduler.TasksRun, Stats.Scheduler.TasksDeferred)And diagnostics (Kernel:enableDiagnostics(1)) publishes the server’s snapshot as attributes for the panel — plus the Kernel.Overload bus topic when deferred work shows up three windows in a row, which Overload will trigger reliably.
Interpreting results
Section titled “Interpreting results”SmallGameis the anchor. Run it first, note step time and GC numbers — that is what a light game costs on this hardware. The HighIntensity delta divided by the rate differences tells you roughly what each op family costs at scale.HighIntensityreading green is the pass condition. It is a realistic busy-shooter profile; a server that clears it has headroom for a real game. Your budget derivation: steady-state kernel step time under HighIntensity is the kernel’s share of a busy frame — everything left under the 16.5ms target belongs to your game logic, physics, and the engine.- Compare your game against the mode it resembles. If your real place’s panel rates (bus msgs/s, hook fires/s, replica writes/s) exceed HighIntensity’s numbers, those rows are your budget lines — use hot tasks to attribute the cost before cutting rates.
Overloadvalidates degradation, not capacity. What to confirm: deferral instead of frame death,Backgroundstarving beforeLow(the burn alternates priorities to make the starvation order visible),Kernel.Overloadfiring, and a clean recovery when you switch the mode off. Never tune your game to survive Overload — it is designed to be unsurvivable.
API reference
Section titled “API reference”| Member | Description |
|---|---|
Simulation.run(kernel, modeName) → Handle |
Starts "HighIntensity", "Overload", "SmallGame", or "Showcase". Errors on anything else. Handle.stop() kills tracked processes, cancels recurring tasks in reverse order, and destroys the visuals folder |
Simulation.Modes |
The ModeConfig table per load mode — read it to derive rates, or to compare your own game’s numbers against |
SimLoad.run(kernel, modeName) → Handle |
Client-or-server shared-primitive load ("HighIntensity" / "SmallGame"); same handle contract. Lives at ReplicatedStorage.ChloeKernel.Debug.SimLoad |
SimPanel.attach(kernel) → boolean |
The SIM CONTROL window; returns false (and builds nothing) when NetTap.debugAllowed says no |
Simulation-owned names
Section titled “Simulation-owned names”The harness’s own traffic is namespaced so it never collides with game channels or topics:
| Name | Kind | Owner |
|---|---|---|
Sim.Intent |
hook point (3 validators) | server load rigs |
SimTelemetry.Tick / SimTelemetry.* |
bus topic + wildcard subscriber | server load rigs |
SimLoad.Intent, SimLoad.Tick / SimLoad.* |
hook point, bus topics | SimLoad rigs |
SimControl, SimNoise |
wire intents | Simulation.server.luau |
ChloeKernelSimulation |
workspace folder | load-mode visuals (ChloeKernelSimLoad<Role> for SimLoad) |
Gotchas & design notes
Section titled “Gotchas & design notes”- Production is gated three ways. The entry script requires Studio or
ChloeKernelDebug = true; theSimControlvalidator additionally honorsChloeKernelDebugUserIds, so flagging a live server does not hand load controls to every player; and the whole surface fails closed — no allowlist means owners/Studio only. - Teardown is total by construction. Hot-swapping modes or setting the attribute to
""kills tracked processes, cancels every recurring handle, disconnects subscriptions, detaches the attached subsystems, and destroys the folder — verified live (see Status). If you build your own rigs on this pattern, copy the reverse-order cleanup list. - The Effects/Zones rigs use
SkipLoop+ manual sweeps so their cadence is owned by cancellable recurring tasks — the same trick your specs can use to make time deterministic.