Skip to content

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 (filter in cl_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 visible test: return false and render isn’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.register only; 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.