<!-- https://minecraftdocs.dev/systems/client/debugging-the-running-game — this page as markdown. The book's index: https://minecraftdocs.dev/llms.txt; the whole book in one file: https://minecraftdocs.dev/llms-full.txt -->

# Debugging the running game

> Verified against **Minecraft 26.3** · Part X · a villager's brain drawn over its head: one subscription mechanism, sixteen instances, all of them in the jar you downloaded and fifteen of them unreachable without a JVM flag.

Every one of these sixteen debug subscriptions is compiled into the shipped
client and the shipped dedicated server. Every subscription is in the
registry, every packet is in the protocol, every producer call site is there.
Nothing is stripped. **The client simply never asks** — fifteen of the sixteen
are behind a JVM system property read at startup, and the server has to agree
besides. The idle cost is small but it is not nothing: the producers check for
a subscriber before they work, and the server sweeps every online player's
permissions once a tick whether or not anyone has asked for anything.

That is the pattern the page is about: a registry of subscription kinds, a per-level engine that sleeps until somebody asks, a poll-and-diff sender, and the renderers — thirteen of the twenty-five — that turn the results into floating text and boxes.
It is only half a client system — the machinery ships on the dedicated server
— but the client is the only thing that ever asks and the only thing that
draws, and the trace ends in a renderer.

## The cast

| class | what it decides | thread |
|---|---|---|
| `DebugSubscription` | what one kind of debug value is: a stream codec, and an expiry | both sides |
| `DebugSubscriptions` | the sixteen kinds | both sides |
| `DebugValueSource` | which objects can answer which subscriptions | Server thread |
| `ServerDebugSubscribers` | who is subscribed, rebuilt every tick, and the permission rule | Server thread |
| `LevelDebugSynchronizers` | per level, a source synchronizer for each of the fifteen subscriptions with a value codec, the event-driven point-of-interest and village-section synchronizers, and the sleep flag | Server thread |
| `TrackingDebugSynchronizer` | the engine: registration, the diff, and the tracking filter | Server thread |
| `ClientDebugSubscriber` | what to ask for, and the maps holding what came back | Render thread |
| `DebugRenderer` | the renderer list, rebuilt when the enabled entries change | Render thread |

## The idea

A `DebugSubscription` is a registry object — held in
`BuiltInRegistries.DEBUG_SUBSCRIPTION` under `Registries.DEBUG_SUBSCRIPTION`
— carrying exactly two things: a nullable stream codec for its value type,
and an expiry in ticks, which is `DebugSubscription.DOES_NOT_EXPIRE` for
most. Its payload wrappers are the records `DebugSubscription.Update` — a
subscription plus an *optional* value, so absence is expressible — and
`DebugSubscription.Event`, a subscription plus a value. Both are serialised
by dispatching on the registry id onto the subscription's own codec.

That is the whole abstraction, and it replaces what used to be a fixed
packet type per kind of information.

`DebugValueSource` is the supply side, implemented by `Entity`, `Mob`, `Bee`,
`Breeze`, `BlockEntity`, `BeehiveBlockEntity` and `LevelChunk`;
`DebugValueSource.registerDebugValues` hands back one
`DebugValueSource.ValueGetter` per subscription the object can answer. On the
client, `ClientDebugSubscriber` keeps what came back, keyed by chunk
position, block position or entity UUID, plus a list of expiring events, and
`ClientDebugSubscriber.createDebugValueAccess` hands renderers a read-only
`DebugValueAccess` view. `DebugRenderer` is a plain list of `DebugRenderer.SimpleDebugRenderer`s,
rebuilt by `DebugRenderer.refreshRendererList`. Neither the synchronizer that
produced the value nor the renderer that consumes it draws anything: the
renderer *appends*, which is the next section.

## A renderer does not draw a gizmo; it appends one

The renderers at the end of this trace emit through `Gizmos`, a debug-drawing
API in the game-engine sense: the immediate-mode *draw me a box in the world
for one frame* facility most engines have and Minecraft did not. `Gizmos` is a
static façade over a **thread-local** `GizmoCollector`, and `Gizmos.addGizmo`
**throws** when no collector is installed on the calling thread — so where you
may draw is decided by where you are running, not by what you are allowed to
see. The shapes are small records — `CuboidGizmo`, `LineGizmo`, `ArrowGizmo`,
`CircleGizmo`, `RectGizmo`, `PointGizmo`, `TextGizmo` — a `GizmoStyle` is a
stroke and a fill, and the `GizmoProperties` handle a call returns can pin the
shape on top, keep it for a number of milliseconds, or fade it.

Five places in the game install a collector; four of them keep what they are given, and those four are the table. The fifth is `GameTestServer`, which installs `GizmoCollector.NOOP`, so code that draws does not throw in a headless test run. The fourth row is the one worth knowing, though nothing the game ships draws into it.

| collector | installed by | what it collects |
|---|---|---|
| per tick | `Minecraft.collectPerTickGizmos` | anything the client tick draws, cleared each tick |
| per frame, during extraction | `LevelExtractor.collectPerFrameMainThreadGizmos` | the debug renderers of this page, inside the extract pass |
| per frame, during rendering | `LevelRenderer.collectPerFrameRenderThreadGizmos` | what the world renderer itself wants outlined |
| **the integrated server** | `IntegratedServer`, around its whole packet-and-tick step | server tick code drawing into a singleplayer world, published for the client to drain |

So **server-side game logic can draw**, and only in singleplayer: a dedicated
server installs no collector at all, so the identical call there would throw.
The two renderers below that want *server* state are singleplayer-only as well: they reach for the singleplayer server directly rather than asking over the wire, and no subscription carries what they draw.

## The sixteen instances

Fourteen rows, sixteen subscriptions: two of the rows carry a matched pair.

| subscription | carries | fed by |
|---|---|---|
| `DebugSubscriptions.BRAINS` | `DebugBrainDump` | `Mob.registerDebugValues` |
| `DebugSubscriptions.GOAL_SELECTORS` | `DebugGoalInfo` | `Mob.registerDebugValues` |
| `DebugSubscriptions.ENTITY_PATHS` | `DebugPathInfo` | `Mob.registerDebugValues`, from the navigator's current `Path` |
| `DebugSubscriptions.BEES` / `DebugSubscriptions.BEE_HIVES` | `DebugBeeInfo` / `DebugHiveInfo` | `Bee` and `BeehiveBlockEntity` |
| `DebugSubscriptions.BREEZES` | `DebugBreezeInfo` | `Breeze` |
| `DebugSubscriptions.POIS` | `DebugPoiInfo` | `TrackingDebugSynchronizer.PoiSynchronizer`, event-driven |
| `DebugSubscriptions.VILLAGE_SECTIONS` | nothing but presence | `TrackingDebugSynchronizer.VillageSectionSynchronizer` |
| `DebugSubscriptions.RAIDS` / `DebugSubscriptions.STRUCTURES` | positions / `DebugStructureInfo` | `LevelChunk.registerDebugValues` |
| `DebugSubscriptions.GAME_EVENT_LISTENERS` | `DebugGameEventListenerInfo` | the listener registry |
| `DebugSubscriptions.GAME_EVENTS` | `DebugGameEventInfo` | the dispatcher — an *event*, expiring after 60 ticks |
| `DebugSubscriptions.NEIGHBOR_UPDATES` | a position | a listener installed on the neighbour updater — an event, 200 ticks |
| `DebugSubscriptions.ENTITY_BLOCK_INTERSECTIONS` | `DebugEntityBlockIntersection` | `Entity`, pushed directly, 100 ticks |
| `DebugSubscriptions.REDSTONE_WIRE_ORIENTATIONS` | an `Orientation` | the experimental wire evaluator, 200 ticks |
| `DebugSubscriptions.DEDICATED_SERVER_TICK_TIME` | **no value at all** | see *the sample path* below |

Four of the sixteen declare an expiry, and "expires after *n* ticks" means two
different things among them. `DebugSubscriptions.GAME_EVENTS` and
`DebugSubscriptions.NEIGHBOR_UPDATES` are *events*, and the number is how long
the event stays on screen. `DebugSubscriptions.ENTITY_BLOCK_INTERSECTIONS` and
`DebugSubscriptions.REDSTONE_WIRE_ORIENTATIONS` are values pushed directly, and
the number is a time-to-live on a stored value. Only the client purges, and
only for those four.

## One instance traced: a villager's brain

```mermaid
sequenceDiagram
    box transparent the client
    participant CDS as ClientDebug<br/>Subscriber
    participant BDR as BrainDebugRenderer
    end
    box transparent the server
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SDS as ServerDebug<br/>Subscribers
    participant LDS as LevelDebug<br/>Synchronizers
    participant TDSS as TrackingDebug<br/>Synchronizer.<br/>SourceSynchronizer
    end

    CDS->>CDS: requestedSubscriptions — the JVM has the brain flag
    CDS->>SGPL: ServerboundDebugSubscriptionRequestPacket, BRAINS
    SGPL->>SGPL: handleDebug<br/>SubscriptionRequest — stored, unhonoured
    Note over SGPL,TDSS: the end of the same server tick
    SDS->>SDS: tick — op, or the owner of an IDE world?
    Note over SGPL,TDSS: the next server tick
    LDS->>LDS: tick — subscribers exist, so wake up
    LDS->>TDSS: registerChunk and registerEntity for everything already tracked
    TDSS->>TDSS: the Mob offers a DebugValueSource.<br/>ValueGetter
    loop each server tick
        TDSS->>TDSS: pollAndSendUpdates — takeBrainDump, compare with the last sent
        TDSS->>CDS: ClientboundDebugEntityValuePacket — only if it differs
    end
    CDS->>CDS: updateEntity — stored under the villager's UUID
    Note over CDS,BDR: the next client frame
    BDR->>BDR: emitGizmos — reads through DebugValueAccess
    BDR->>BDR: Gizmos.<br/>billboardTextOverMob — appended now, drawn later
```

*Two sides and, from a sleeping level, one tick of lag before anything is sent: the request is stored at the head of a server tick and the subscriber set read at its end, and the synchronizers wake, and first send, in the next. The packet in the
loop reaches `ClientDebugSubscriber` through `ClientPacketListener` like any
other, which is why no listener lane is drawn.*

The figure's own names, in its order:
`ClientDebugSubscriber.requestedSubscriptions` is what the JVM flag filled;
`ServerGamePacketListenerImpl.handleDebugSubscriptionRequest` stores the set
on the `ServerPlayer`; `LevelDebugSynchronizers` calls `TrackingDebugSynchronizer.SourceSynchronizer.registerChunk` and `TrackingDebugSynchronizer.SourceSynchronizer.registerEntity` for everything already tracked; `TrackingDebugSynchronizer.SourceSynchronizer.pollAndSendUpdates` is the per-tick compare, and for a villager the value it asks for is a brain dump;
`ClientDebugSubscriber.updateEntity` files the answer under the entity's
uuid; and `BrainDebugRenderer` reads it back next frame.

The engine is the synchronizers' part of that, and it has three properties worth naming. **Nothing exists until somebody asks**: the level's synchronizers
start asleep, and the first non-empty subscriber set wakes them and
retroactively registers every ready chunk and every tracked entity.
**A polled value is sent again only when it changes**: each value source keeps the last value it sent and compares, and a player who starts tracking is sent that stored value once. The point-of-interest synchronizers and the pushed kinds send on each event instead. And **nothing reaches a player who cannot see it**: sending is
filtered by subscription *and* by whether that player is tracking the chunk
or entity. When the last subscriber goes away the value sources are cleared and the level goes back to sleep.

The three cadences: `ClientDebugSubscriber.tick` runs from
`ClientPacketListener.tick`, once per client tick, and sends only when the
wanted set differs from the last one sent.
`ServerDebugSubscribers.tick` runs from [`MinecraftServer.tickChildren`](../server/server-tick#what-minecraftservertickchildren-runs-and-in-what-order)
*after* the levels have ticked, while each `LevelDebugSynchronizers.tick`
runs *inside* its level's tick — so **every level acts on the previous tick's
subscriber snapshot**, a built-in one-tick lag. And `DebugRenderer.emitGizmos`
runs inside `LevelExtractor.extract` — the snapshot step of [the frame](../rendering/the-frame#the-zones-a-frame-is-made-of) — after entities, block entities, particles, sky and clouds,
fetching one `DebugValueAccess` for the whole pass.

## The exceptions

Every pattern page's real content.

**Two gates, and the second is not a flag.** Fifteen of the sixteen kinds are
behind `SharedConstants.DEBUG_ENABLED` *and* one of a set of constants beside it —
`SharedConstants.DEBUG_BRAIN`, `SharedConstants.DEBUG_POI`,
`SharedConstants.DEBUG_BEES` and their siblings, thirteen for the fifteen kinds, all read from JVM system properties at startup, so turning one on means launching the game
differently rather than pressing anything. The only subscription an F3 key can
reach is the dedicated server's tick time, through the FPS charts. And
the server still has to agree: `ServerPlayer.debugSubscriptions` returns
nothing unless `ServerDebugSubscribers.hasRequiredPermissions` passes, which
means op on the player list, or the owner of a singleplayer world run from an
IDE. On a normal singleplayer world that means cheats must be on.

**Producers check before they work.** Path finding only records its open and
closed node sets when somebody wants paths; entities only collect block
intersections when somebody wants them; the neighbour updater's debug
listener is only installed while someone is subscribed. There are two gates
of that name and the producers do not agree on which to use: path finding and
block intersections ask `ServerDebugSubscribers.hasAnySubscriberFor`, the live
map, while the neighbour updater asks
`LevelDebugSynchronizers.hasAnySubscriberFor`, which reads the level's
snapshot from the previous tick. The change detection, by contrast, is
*record equality*: a
brain dump is rebuilt every tick per villager and compared with the last one
sent — so the saving is in bandwidth, not in server time.

**The other twelve renderers do not use this system at all.** The chunk debug
renderer reaches directly into `Minecraft.getSingleplayerServer` and shows
nothing in multiplayer; the entity hitbox renderer reaches for it too, but only to check the server has the entity, for its flag-gated *server* hitbox; the ordinary hitbox it draws itself, switched by an F3 entry rather than a flag, as [entity rendering](../rendering/entity-rendering) says. And a
whole family of them — chunk borders, light, collision boxes, height maps,
the section octree — are purely client-side views that need no server.

**One flag combination under-delivers, and says so.** The POI renderer's
ticket-holder rows are behind an explicit `SharedConstants.DEBUG_BRAIN` test,
so running with the POI flag alone gives POI boxes with their own two labels
and none of the brain ones. The bee flag avoids needing the test at all by
explicitly also requesting goal selectors.

**Subscriptions survive a dimension change and a death, but not a
reconnect.** A dimension change keeps the same `ServerPlayer`, so the set is
simply never touched; a respawn builds a new one and `ServerPlayer.restoreFrom`
copies the requested set across. A fresh login starts empty, and the client
re-sends on its next tick because `ClientDebugSubscriber` was cleared at
login.

## The other query, which is one transaction deep

Not everything the client asks the server for is a subscription. Press the
copy-recreate-command key — `Options.keyDebugCopyRecreateCommand`, an ordinary
rebindable mapping — while looking at a block or an entity and the client
builds a `/setblock` or `/summon` for it and puts it on the clipboard. Whether
the NBT in that command is *yours* or the *server's* is decided by two things.
Holding shift takes the client's own copy, straight out of the block entity or
the entity in front of you. Not holding it goes to the server, through
`DebugQueryHandler`: `ServerboundBlockEntityTagQueryPacket` or
`ServerboundEntityTagQueryPacket` with a transaction id, a callback parked
against that id, and the command assembled when the reply comes back. The
handler holds **one** callback and one id, so a second query cancels the first
by overwriting it, and a reply whose id does not match is dropped on the
floor. Including the NBT at all needs the gamemaster permission, and the whole
key is dead while the server has reduced debug info on.

## The sample path, which shares only a subscription

The performance charts are a separate and much simpler system, and what they have in common with everything above is the subscriber side: one of the sixteen subscriptions gates the remote half of them, asked for in the same request and granted by the same permission rule. There is no shared engine, no
diff and no tracking filter. A `SampleLogger` takes a vector of longs; a tick
logs its parts as it goes and a final call flushes the whole vector. There are two implementations
and the difference is the whole story: `LocalSampleLogger` **is** the storage
— a `SampleStorage` ring buffer the charts read directly — while
`RemoteSampleLogger` stores nothing and broadcasts a
`ClientboundDebugSamplePacket` if anyone is subscribed.

So a dedicated server measures its tick with a remote logger and sends, while
`IntegratedServer.getTickTimeLogger` hands back **the client's own** local
logger and reports logging as unconditionally enabled — singleplayer TPS
never touches the network. Client-side, `DebugScreenOverlay` owns four local
loggers with four different feeders: the frame time from the loop, the tick
time from either of the two paths above, the ping from `PingDebugMonitor`
(which sends its own ping requests, and only while the network charts are
shown), and the bandwidth from a `BandwidthDebugMonitor` that counts bytes on
the Netty thread and is drained by `Connection.tick`.

Six packets carry the subscriptions and the samples between them:
`ServerboundDebugSubscriptionRequestPacket` outbound, and
`ClientboundDebugChunkValuePacket`, `ClientboundDebugBlockValuePacket`,
`ClientboundDebugEntityValuePacket`, `ClientboundDebugEventPacket` and
`ClientboundDebugSamplePacket` inbound — for a system that used to have one
packet per subject. The two tag-query packets of the section before are not
among them, and are not part of this machinery at all.

> **For a 1.21-era reader.** The fixed set of debug packets is gone. Instead
> of one packet type per kind of debug information there is one
> `DebugSubscription` registry and three generic value packets that dispatch
> on the registry id. The debug *screen* is a different system again — the F3
> entry registry described in [the HUD](hud) — and it is not a light
> touch: the F3 entries decide whether eleven of the twenty-five renderers
> exist at all when the list is rebuilt, and the FPS charts gate the tick-time
> subscription.

## Where to look

`DebugSubscriptions` for the catalogue and `DebugSubscription` for how little
a subscription is. `TrackingDebugSynchronizer` for the engine — the tracking diff and the equality check are in that one class, and the wake-up's back-fill is `LevelDebugSynchronizers`'.
`LevelDebugSynchronizers.tick` for the sleep flag, `ClientDebugSubscriber`
for both ends of the client's half, and `DebugRenderer.refreshRendererList`
for which renderers exist and why.

---

*Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes `tools/verify_names.py`.*
