Builtin Library
README.md
profiler Module
Public Luau surface over the __profiler Internal FFI namespace —
frame-level profiler capture, EMA stats, and named blocks.
Purpose
Wrap the raw __profiler.* FFI namespace in a typed Luau table that
gets auto-injected as _G.profiler via the prelude. Captures persist
to VFS at /zero/runtime/profiler/<label>.json; EMA stats and named
blocks land back in the engine's SystemProfiler resource so they're
visible in captures and in the in-engine profiler panel.
Usage
-- Capture a window of frames. One recording runs at a time engine-wide,
-- and several agent sessions share one engine, so a refused start is
-- answered with who holds it rather than with the bare fact that
-- something does.
if not profiler.startCapture("hitch") then
print("held by", profiler.recordingHold())
return
end
task.wait(2)
local json = profiler.stopCapture()
print(json)
-- Read live EMA stats. `avg_frame_ms` is the block's share of the
-- current frame; `avg_ms` is what one run of it costs.
for _, s in ipairs(profiler.stats("schedule.*")) do
print(s.name, s.avg_frame_ms, s.avg_ms, s.max_ms)
end
-- Named blocks
profiler.begin("ai.tick")
runTick()
profiler.finish("ai.tick")
-- Or scope it with measure() (handles errors)
local result = profiler.measure("ai.tick", function() return runTick() end)
Exports
profiler.startCapture(label?) -> boolean. Takes the one engine-wide recording slot for the calling caller. False when another recording holds it;profiler.recordingHold()then names what does.profiler.recordingHold() -> string?. JSON{ label, owner, started_at, held_seconds }for the live recording, or nil when none runs.owneris the caller key that took the slot, the same valuevfs.currentAuthor().idreads for the calling caller, so a caller refused the slot tells its own earlier recording from a sibling session's.profiler.stopCapture() -> string?. JSON capture summary.callerscarries each caller's share of the window's queued Luau, and each frame carries its own split asagent_by_caller.profiler.isCapturing() -> booleanprofiler.lastCapture() -> string?profiler.stats(pattern?) -> { ProfilerStat }— live EMA stats. Each entry carries two averages:avg_msaverages one RUN of the block, folded when the block runs, so a block that has stopped running keeps the last value it saw;avg_frame_msaverages one FRAME, folded every frame including the frames the block did not run in, so it is the block's share of the current frame and falls back to zero once the block stops running.profiler.counters(pattern?) -> { ProfilerCount }— unitless per-frame tallies kept apart from the millisecond timings ofstats, for example the engine thread's waits on the process-wide lock undersystem.schedule.frame.process_local_waits, and the queue submissions the renderer and the UI made undersystem.schedule.frame.queue_submissions(each one is a fixed cost to the GPU driver, whatever it carries), and the uploads the renderer's device staged: the buffer and texture writes undersystem.schedule.frame.queue_buffer_writesandsystem.schedule.frame.queue_texture_writes, and the staging buffers the device created to carry them undersystem.schedule.frame.staging_buffer_creations(the device stages a frame's writes in chunks it reuses, so a frame whose writes fit the chunks it holds creates none; in a browser's WebGPU the browser stages the writes and the three read zero), and the maps of the renderer's statistic readbacks undersystem.schedule.frame.gpu_readbacks, with each readback's own maps under that key followed by its name:hiz.occlusion_countsandinstanced_compaction.submitted(the countsrenderer.cullStats().occlusionTested/occlusionCulledandrenderer.drawStats().compactedDrawnanswer, read back on the two rendered frames after each read and one frame in 60 otherwise),cluster_cull.counters,visibility.resolve_countersandgpu_timing.readback(sosystem.schedule.frame.gpu_readbacks.hiz.occlusion_countscounts the occlusion counts' maps), and the profiler names resolved by hashing them undersystem.profiler.name_lookups: a measurement's name is resolved the first time it is recorded and found by its key after that, so a frame recording only measurements it has recorded before counts none (aprofiler.begin/profiler.measureblock finds its key by the Luau string naming it, so a name a script builds again after its last string was collected is resolved again).frameis the tally of the frame in progress,prev_framethe last complete frame's,max_framethe highest complete-frame tally andtotalthe tally since the profiler started.profiler.begin(name)/profiler.finish(name?) -> number?— paired named blocks, LIFO by default.profiler.measure(name, fn) -> ...— scoped block around a function.