Benchmarks
Measured per-operation cost of the kernel's hot paths (token buckets, hooks, the Bus, signals, the scheduler and Serde) and what the numbers mean for a frame budget.
The kernel’s hot paths are measured with Bench.runAll over the modules in
ReplicatedStorage.ChloeKernel.Benches. These are microbenchmarks that time one call in a tight loop.
Performance covers what the framework costs at idle and under load, and the limits it
enforces.
| Operation | Throughput | p50 per op |
|---|---|---|
TokenBucket:take |
17.0M ops/s | 58 ns |
Hooks:fire (3-validator chain) |
6.6M ops/s | 148 ns |
Bus:publish (exact topic) |
5.0M ops/s | 198 ns |
Signal:fire (1 handler) |
4.5M ops/s | 203 ns |
| Scheduler schedule and step (MicroProfiler zones off) | 2.2M ops/s | 354 ns |
| Scheduler schedule and step (zones on, the Studio default) | 2.1M ops/s | 388 ns |
| Serde schema encode (3 fixed fields) | 3.5M ops/s | 285 ns |
| Serde schema decode (3 fixed fields) | 2.7M ops/s | 367 ns |
| Serde struct delta encode (2 fields) | 3.7M ops/s | 270 ns |
| Serde struct delta decode (2 fields) | 3.4M ops/s | 293 ns |
Reading the numbers
Section titled “Reading the numbers”- A client payload’s validation (a per-channel token bucket plus a three-validator hook chain) costs about 200 ns, well under a microsecond. Rejecting garbage is cheap, and there is no performance argument for skipping validation. Decode cost depends on the schema; see Serde and Packet and wire types.
- The hot-task profiler attributes each scheduled callback’s time to the
script:linewhere the function was defined. Origins are cached by function identity, so steady-state attribution is one table lookup; the profiler fix that produced this table took schedule-and-step from 2,848 ns to 354 ns, about 33 ns over the 321 ns baseline without profiling. Since 0.8.0,Kernel.boot({ ProfileTasks = false })skips the per-task clock reads altogether, andkernel:enableDiagnostics()turns profiling back on. - MicroProfiler zones (
debug.profilebeginandprofileendaround every task, named by origin) add about 34 ns per task in this bench. They are on by default only in Studio. A live server opts in withScheduler.setMicroProfilerZones(true). - The Serde fast engine against the wire-engine path on the same machine: fixed-schema encode went from 463 ns to 285 ns (38 percent faster), decode from 514 ns to 367 ns, and dynamic schemas got 22 to 27 percent faster, with identical bytes either way.
Bus:publishto a topic with no wildcard subscribers allocates nothing: the no-match case returns a shared frozen table.
Running the suite
Section titled “Running the suite”Bench.runAll(container) requires every ModuleScript under the container whose name ends in .bench, sorted by name,
and runs each registered case: 1,000 warm-up calls, then batches of 1,000 calls for 0.25 seconds. p50 and p99 are
taken over the per-batch mean, and one frame passes between cases so they do not contend. Each result prints as one
[Bench] line with ops per second, p50 and p99.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Bench = require(ReplicatedStorage.ChloeKernel.TestKit.Bench)
local Results = Bench.runAll(ReplicatedStorage.ChloeKernel.Benches)for _, Result in Results do print(Result.Name, Result.OpsPerSecond, Result.P50Nanos, Result.P99Nanos)endThe runtime release model leaves out TestKit and Benches, so run the suite from the development project in
Studio. Put game benchmarks in their own .bench modules and pass their folder to runAll; see
Benchmark your own systems.