Performance rules
Status: Partly implemented. The rules below apply today, and the engine enforces a memory limit and a time limit per callback. Per-tick and per-frame budgets with an F3 page, deferrable subscriptions, batch edits, snapshots and worker jobs are Planned.
Celestial is built to be much faster than the original game: no frame hitches, near-zero idle CPU, memory well below vanilla’s. A mod runs inside those same tick and frame loops, so one slow handler can undo the engine’s work.
What the engine enforces
| Limit | Default | Manifest |
|---|---|---|
| Memory | 256 MB (at most 4096, WASM and Lua) | limits.memory_mb |
| One Lua callback | 50 ms; then it is stopped with an error | |
| One WASM call | a deadline (epoch interruption); then it traps | |
| Errors | Lua: 10 within a minute; WASM: 10 traps, or one call past its deadline, turn the mod off for the session |
The limits are ceilings for bad moments, not targets. A tick has 50 ms in total and Celestial’s own tick uses a few; a frame at high FPS lasts well under a millisecond. A well-behaved mod averages microseconds.
The rules
1. Do nothing when you have nothing to do
- Filter frequent events (
filterincl_subscribe_opts, Lua{ filter = ... }): a filtered handler is never called for other blocks or entities. - Subscribe only while a feature is on, and cancel the subscription when it turns off. A disabled client feature should cost nothing (A client mod like the built-in client settings features).
- HUD layers have a
visibletest: return false andrenderisn’t called. - Avoid
tick.*handlers that poll. React to events, or schedule a task for when you need it.
2. Compute on the tick, draw on the frame
HUD layers and screen render callbacks run every frame. Count, search and format on tick.client_post (20 times a second), keep the result, and only draw in the frame.
3. Split long work
One callback holds up the thread that called it (and the per-mod lock). Split long work into steps with the scheduler (cl_sched_run_server, clight::every, Lua cl.spawn + cl.sleep_ticks), a few hundred blocks or entities per tick.
4. Send less
Send payloads on change, not every tick, and combine changes into one payload per tick.
5. Cache lookups, not world state
Intern identifiers once, read config into variables when it changes (config.reloaded). Don’t cache block states or entity positions across ticks without checking them again; keep UUIDs, not handles, for long-lived data.
6. Don’t allocate in hot paths
Per-tick and per-frame code should not create tables (Lua) or allocate (C, C++). Reuse buffers and tables.
7. Keep work in your own code
Each API call crosses the sandbox boundary. In C and C++, do the maths in your own code and call the API for results; in Lua, cache library functions in locals in hot loops.
8. No threads of your own
Mods have no threads; the engine’s job system for mods is planned (The job system).
9. Data first
Recipes, loot, tags, enchantments, models and textures as data cost nothing at run time (Data-driven content).
10. Measure in a real world
Measure with cl_time_now_ns() around your code and log it (Profiling mods). Test in a real survival world or on a busy server, not an empty test world: event rates differ by orders of magnitude.
Event rates
Typical rates on a busy survival server, to see where filters matter:
| Event | Rate |
|---|---|
tick.client_post, tick.server_post |
20 per second |
tick.level_pre / level_post |
20 per second per level |
entity.damage |
hundreds per second in mob farms |
block.break, block.place |
a few per second per active player |
client.sound_play |
dozens per second |
HUD render |
your FPS (hundreds to thousands per second) |
Memory rules
- Free per-world data in
lifecycle.server_stopped: singleplayer opens and closes worlds many times in one process. - Release owned values ©, or let RAII /
<close>do it. - Keep textures small; integrated GPUs have little memory.
Startup rules
- Register and subscribe in construct and
lifecycle.registeronly; don’t load big files there. - Rebuild asset-dependent caches in
lifecycle.resources_reloaded, and only what changed.
Planned
Per-tick and per-frame budgets per mod (limits.tick_ms, limits.frame_ms) with an F3 page and log warnings, deferrable subscriptions (CL_SUB_DEFERRABLE) that pause under load, batch block edits, world snapshots for background work, worker jobs and parallel loops.