Skip to content

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

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.

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, … 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/FireClient no-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=900KB

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

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.

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 SimControl intent.
  • Client load (this machine) — runs Debug/SimLoad locally: 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. Modes HighIntensity (150 entities, 20 bolts/s, 400 bus msgs/s, 240 hook fires/s, 80 serde ops/s, 2ms burn, 60 intents/s) and SmallGame (~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.

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 as Process 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, before Deferred goes 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 — SimNoise traffic with RateLimited verdicts 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.

  • SmallGame is 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.
  • HighIntensity reading 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.
  • Overload validates degradation, not capacity. What to confirm: deferral instead of frame death, Background starving before Low (the burn alternates priorities to make the starvation order visible), Kernel.Overload firing, and a clean recovery when you switch the mode off. Never tune your game to survive Overload — it is designed to be unsurvivable.
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

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)
  • Production is gated three ways. The entry script requires Studio or ChloeKernelDebug = true; the SimControl validator additionally honors ChloeKernelDebugUserIds, 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.