Bench
Bench answers one question per line: how many operations per second, and what does one operation cost at the median and the tail. It shares TestKit’s discovery convention — *.bench ModuleScripts, sorted by name — but answers a different question: TestKit checks whether your code is correct, Bench checks how fast it is.
Timing methodology
Section titled “Timing methodology”Bench.run(name, fn, options?):
- Runs
Setupif provided, then a warmup batch of 1,000 calls — cold-cache and cold-path effects stay out of the numbers. - Runs timed batches of 1,000 calls until the deadline (
DurationSeconds, default0.25). Each batch records(batch elapsed) / 1000as its per-op latency. - Sorts the batch timings: p50 and p99 are percentiles over batch-averaged per-op latency, and
OpsPerSecondis total ops over total elapsed.
fn receives the index within the batch (1–1000) — use it to vary inputs (Items:add(index % 50, 1)) so you are not benchmarking a warm branch on one constant.
Write your own bench
Section titled “Write your own bench”A bench module is a ModuleScript named <Thing>.bench that returns function(bench). The framework’s own suite lives in ChloeKernel/Benches/Kernel.bench.luau; a minimal one of yours:
-- Inventory.bench — ModuleScript under ReplicatedStorage.ChloeKernel.Bencheslocal Inventory = require(game:GetService("ReplicatedStorage").Game.Inventory)
return function(bench) local Items = Inventory.new()
bench("Inventory:add", function(index) Items:add(index % 50, 1) end)
bench("Inventory:serialize", function() Items:serialize() end, { DurationSeconds = 0.5 })endBenches do not run at boot. Run them on demand — the command bar during a play test works:
local Bench = require(game.ReplicatedStorage.ChloeKernel.TestKit.Bench)Bench.runAll(game.ReplicatedStorage.ChloeKernel.Benches)runAll collects every *.bench descendant, sorted by name, and yields one frame between benches so consecutive benchmarks do not contend with each other’s deferred work.
Reading the output
Section titled “Reading the output”[Bench] running 1 benchmark modules...[Bench] Signal:fire (1 handler) 12.41M ops/s p50/op 78ns p99/op 121ns[Bench] Serde schema encode (3 fields) 1.02M ops/s p50/op 942ns p99/op 1.31µs- ops/s — throughput over the whole timed window; the headline number for “can I afford this in a loop”.
- p50/op — the typical cost of one call. Multiply by your per-frame call count to get a frame cost.
- p99/op — the sustained-tail cost. A p99 far above p50 means the operation allocates (GC assists land in some batches) or hits a slow path intermittently — worth knowing before it runs 500 times a frame.
The framework’s published numbers on Benchmarks were measured with exactly this harness, so your results are directly comparable on your hardware.
API reference
Section titled “API reference”| Member | Description |
|---|---|
Bench.run(name: string, fn: (index: number) -> (), options?: { DurationSeconds: number?, Setup: (() -> ())? }): Result |
One benchmark: warmup batch, then timed 1,000-call batches until the deadline (default 0.25s). |
Bench.runAll(container: Instance): { Result } |
Runs every *.bench module descendant (sorted by name), printing one aligned row per bench, one frame apart. |
Result |
{ Name, OpsPerSecond, P50Nanos, P99Nanos, TotalOps } |
For correctness testing (not performance), see TestKit.