Skip to content

Profiler

You cannot make a server faster until you can see where its tick goes. EmberMC times the tick all the time, cheaply, and lets you turn on the expensive part when you need it.

Two layers, priced differently

Phase timing is always on. Every tick is split into phases - scheduler, block ticks, chunks, block events, entities, block entities, connections (inbound packets handled and outbound flushed, including commands players ran), console commands, and whatever is left - each timed with one nanoTime call and one addition. The phases never overlap, so a report always adds up to the tick. With three worlds that is about forty calls a tick, around a microsecond in total. The last sixty seconds of every phase, for every world, are kept in fixed rings that never allocate on the tick.

Plugin attribution is opt-in. Timing every event handler and every scheduler task costs two nanoTime calls per call, and a busy server fires thousands of events a tick. So it runs only during a session you start, guarded by a single flag read at each site, and stops itself when the session ends.

Commands

Command What it shows
/ember profiler Every phase: mean and p95 over the last 5 s, p99 over the last minute
/ember profiler start [seconds] Begin attributing time to plugins (default 60 s; 0 = until stopped)
/ember profiler stop End the session; /ember plugins keeps the results
/ember plugins ms per tick, event calls and task runs per plugin over the session
/ember worlds Per world: tick ms, entities, chunks, block entities, heaviest phase
/ember entities Entity counts by type, full/reduced tier split this tick, and pathfinds skipped
/ember chunks Loaded / ticking / force-loaded / plugin-held chunks per world, and which plugins hold them
/ember metrics Every gauge EmberMC exposes, named ember_*
/ember netstat start [sec] | stop Time-boxed sample of outbound traffic by category (off by default, zero cost)
/ember bench [seconds] Measure the entity tiers on your current load, vanilla vs each tier
/ember doctor Read every number at once and list what is costing you and what to change

All under ember.command.<name>, default operators.

What /ember plugins does and does not measure

It measures main-thread time spent inside a plugin's event handlers and synchronous scheduler tasks. That is where most plugin lag lives, and it is attributed exactly.

It does not see: time inside a command the plugin registered, a packet listener, an entity the plugin spawned, a chunk the plugin keeps loaded, or anything the plugin does off the main thread (which cannot lag the tick anyway). A plugin can be expensive in those ways and show a low number here. The command says so under its table, every time, so nobody is misled into blaming the wrong plugin.

The spike watchdog

Nobody is watching when the bad tick happens. The watchdog is: on every tick end it compares the tick against profiler.spike-threshold-ms (100 by default) and, when it trips, writes down what that tick was doing:

  • every phase's share, in ms and percent
  • every world's tick and its phases, with entity, chunk and block-entity counts
  • whether a garbage collection ran during the tick, and for how long
  • the heaviest plugins in that tick, if a session was running
  • the last five seconds of context, so a one-off can be told from a trend

One line goes to the console; the full report goes to ember-reports/spike-<timestamp>.txt. Reports are rate-limited (spike-report-cooldown-seconds, 30) and pruned (keep-spike-reports, 50), so a sustained overload produces a handful of files rather than thousands.

[WARN]: Tick took 187.42 ms (threshold 100 ms); heaviest phase: entities 141.07 ms, GC ran 1x (38 ms). Report: ember-reports/spike-2026-09-05_06.41.12.txt

Metrics

/ember metrics reads a registry of named gauges - TPS, MSPT, players, entities, chunks, block entities, each phase's 5-second mean, the tick's one-minute p99, and whether a session is running. A gauge is a supplier, not a sample: nothing is collected until something reads it, so the registry is free when idle. The same gauges are served in Prometheus text format at http://127.0.0.1:9464/metrics when metrics.endpoint.enabled is set - off by default, bound to localhost by default, one daemon thread, nothing collected between scrapes.

Configuration

profiler:
  spike-threshold-ms: 100            # a tick longer than this is a spike. Reload-safe.
  spike-report-cooldown-seconds: 30  # at most one report per this long. Reload-safe.
  keep-spike-reports: 50             # oldest are deleted. Reload-safe.
  spike-reports-dir: ember-reports   # relative to the server directory. Reload-safe.
  default-session-seconds: 60        # /ember profiler start with no duration. Reload-safe.