Skip to content

Bench

Microbenchmark harness that times 1,000-call batches after a warmup and reports ops per second with p50 and p99 per-op cost.

Realm
Shared
Require
ReplicatedStorage.ChloeKernel.TestKit.Bench
Since
v0.1.0
Source
TestKit/Bench.luau

Bench answers one question per line: how many operations per second, and what one operation costs at the median and at the tail. It shares TestKit’s discovery convention (*.bench ModuleScripts, sorted by name) and answers a different question: TestKit checks that code is correct, Bench measures how fast it is. The numbers on Benchmarks come from this harness running the framework’s own Kernel.bench.

Bench.run(name, fn, options?):

  1. Runs options.Setup if given, then one warmup batch of 1,000 calls, so cold caches and first-call paths stay out of the numbers.
  2. Runs timed batches of 1,000 calls until the deadline (DurationSeconds, default 0.25). Each batch records its elapsed time divided by 1,000 as that batch’s per-op cost.
  3. Sorts the batch costs. P50Nanos and P99Nanos are percentiles over those batch averages, and OpsPerSecond is total calls over total elapsed time.

fn receives its index within the batch (1 to 1000). Use it to vary input (Items:add(index % 50, 1)) so the bench does not measure one warm branch on one constant.

A bench module is a ModuleScript named <Thing>.bench that returns function(bench). Each bench(name, fn, options?) call inside it runs one benchmark. The framework’s suite is ChloeKernel/Benches/Kernel.bench.luau. A game bench looks the same:

src/Server/Game/Benches/Codec.bench.luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Serde = require(ReplicatedStorage.ChloeKernel.Serde)
return function(bench)
local Loadout = { Weapon = "Sword", Level = 12, Perks = { 3, 7, 9 } }
local Encoded = Serde.encode(Loadout)
bench("Serde encode (loadout)", function()
Serde.encode(Loadout)
end)
bench("Serde decode (loadout)", function()
Serde.decode(Encoded)
end, { DurationSeconds = 0.5 })
end

Benches never run at boot. Run them on demand, for example from the command bar during a Play session:

local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Bench = require(ReplicatedStorage.ChloeKernel.TestKit.Bench)
Bench.runAll(ServerScriptService.Game.Benches)

runAll collects every *.bench descendant, sorts by name, requires each module and calls it with a bench function. It waits one frame (task.wait()) after each benchmark so one bench’s deferred work does not land inside the next one’s timing.

runAll prints a header line with the module count, then one aligned row per benchmark. The shape, with numbers from one machine:

[Bench] running 1 benchmark modules...
[Bench] Signal:fire (1 handler) 12.41M ops/s p50/op 78ns p99/op 121ns
[Bench] Hooks:fire (3-validator chain) 3.12M ops/s p50/op 309ns p99/op 402ns
  • ops/s is throughput over the whole timed window. It answers whether a call is affordable inside a loop.
  • p50/op is the typical cost of one call. Multiply it by calls per frame to get a frame cost.
  • p99/op is the sustained tail. A p99 far above p50 means the operation allocates (GC assists land in some batches) or takes a slow path now and then. Know that before it runs 500 times a frame.

Units switch at 1,000: nanoseconds, then microseconds, then milliseconds, and ops/s, K ops/s, M ops/s.

Member Description
Bench.run(name: string, fn: (index: number) -> (), options: { DurationSeconds: number?, Setup: (() -> ())? }?): Result One benchmark: a warmup batch, then timed 1,000-call batches until the deadline (default 0.25 s). Prints nothing.
Bench.runAll(container: Instance): { Result } Runs every *.bench module under container, sorted by name, printing one row per benchmark and waiting one frame between benchmarks.
Result { Name, OpsPerSecond, P50Nanos, P99Nanos, TotalOps }

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