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.
Timing methodology
Section titled “Timing methodology”Bench.run(name, fn, options?):
- Runs
options.Setupif given, then one warmup batch of 1,000 calls, so cold caches and first-call paths stay out of the numbers. - Runs timed batches of 1,000 calls until the deadline (
DurationSeconds, default0.25). Each batch records its elapsed time divided by 1,000 as that batch’s per-op cost. - Sorts the batch costs.
P50NanosandP99Nanosare percentiles over those batch averages, andOpsPerSecondis 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.
Write your own bench
Section titled “Write your own bench”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:
--!strictlocal 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 })endBenches 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.
Reading the output
Section titled “Reading the output”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 402nsops/sis throughput over the whole timed window. It answers whether a call is affordable inside a loop.p50/opis the typical cost of one call. Multiply it by calls per frame to get a frame cost.p99/opis 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.
API reference
Section titled “API reference”| 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 } |