Inspector

CNA snapshot 009d40f5  ·  new since alpha.1  ·  opt-in: CNA_BUILD_INSPECTOR

ℹ

New in the development snapshot; a development tool, not a feature of your shipped game. The Inspector does not exist in the alpha.1 tag. It is opt-in (CNA_BUILD_INSPECTOR, default OFF), desktop-only, view-only, and no CI workflow builds it. Everything below is read from the source at snapshot 009d40f5. It needs Diagnostics to be enabled to show anything useful, and the timing figures in Performance are CNA’s own, from one machine.

What the Inspector is

The Inspector lets you watch a running CNA application from a browser: frame rate and frame times, draw-call and sprite counters, CPU zones, live resources and an event log. It observes the process through the version-1 diagnostics provider and has two parts:

  • an in-process agent (CNA::Inspector::Agent): one background thread, an authenticated TCP endpoint and a compact binary protocol. The agent never embeds a web server and never builds JSON;
  • a separate cna-inspector bridge process that talks to the agent, serves an offline browser UI at http://127.0.0.1:<port>/ and translates for the browser.

It is view-only. The protocol has exactly the messages listed below: no editing, no scripting, no input injection, no arbitrary memory or method access. It is not an editor, a debugger or a remote-administration tool.

Architecture

  your game process                              separate process             your browser
+--------------------------------+   TCP    +---------------------+   HTTP    +-----------+
| Diagnostics provider (v1)      |  (auth,  | cna-inspector       | 127.0.0.1 | 8-view UI |
|   ^ pull                       | binary)  |   (bridge)          |  only     | (offline  |
| Inspector agent (one thread)   |<-------->|   web server        |<--------->|  assets)  |
+--------------------------------+          +---------------------+           +-----------+
   listens only after Agent::Start()          reads the token from env/file     UI token per bridge run

Without a call to Agent::Start() there is no listener, no thread and no work. A started agent with no connected client blocks in the operating system and does not poll the provider. Splitting the web server into its own process keeps HTTP parsing, JSON and the browser out of the game entirely.

Build and activation

WhatHow
Enable the module-DCNA_BUILD_INSPECTOR=ON (default OFF). It builds the static library cna_inspector (CNA::Inspector), the bridge executable cna-inspector and, with CNA_BUILD_EXAMPLES (default ON), the demo cna_inspector_demo. CNA_BUILD_BENCHMARKS adds the benchmarks.
Give it dataCombine it with -DCNA_DIAGNOSTICS=STATS or FULL. STATS yields snapshot and resource capabilities; FULL adds events and CPU zones. With OFF a session negotiates but shows almost nothing.
Link itThe module is not in the CNA umbrella. A game links CNA CNA::Inspector explicitly; without that, no Inspector code reaches the binary.
Start itExplicit code: CNA::Inspector::Agent::Start(configuration, error) returns a std::unique_ptr<Agent>, or null with error set. There is no static initializer, no environment hook and no automatic Game change.
PlatformsDesktop Windows, Linux and macOS/POSIX only. CNA_BUILD_INSPECTOR=ON is a configure-time FATAL_ERROR on Emscripten, Android and iOS. Only the standard library and OS sockets/random are used (POSIX sockets and /dev/urandom; Winsock2 and BCryptGenRandom on Windows).
RendererNone required. The demo runs under any platform, including HEADLESS.
# In your game's CMakeLists.txt (CNA added with add_subdirectory)
target_link_libraries(my_game PRIVATE CNA CNA::Inspector)
#include "CNA/Inspector/Agent.hpp"

CNA::Inspector::AgentConfiguration configuration;
configuration.applicationName = "My game";
configuration.metadata = {{"Resolution", "1920x1080"}};

std::string error;
auto inspector = CNA::Inspector::Agent::Start(configuration, error);   // keep it alive for the whole run
if (inspector) {
    // hand inspector->GetPort() and inspector->GetAuthenticationToken() to the developer locally
}

AgentConfiguration

FieldDefaultMeaning
bindAddress"127.0.0.1"Where the agent listens. A non-loopback address is rejected unless allowRemote is true.
port00 selects an ephemeral port; read it back with GetPort().
allowRemotefalsePermit a non-loopback bindAddress (see Security).
authenticationTokenemptyWhen empty, a 256-bit token is generated from the OS secure random source; read it with GetAuthenticationToken(). An explicit token may be at most 256 bytes.
applicationName"CNA application"Shown in the Session view.
platformBackend, buildConfigurationauto-filledFilled from the build when empty.
metadatanoneUp to 64 key/value pairs (key at most 128 bytes, value at most 1,024 bytes).
previewProvidernullOptional IResourcePreviewProvider (see Previews).
maximumRequestsPerSecond64Clamped to 1–1024.
maximumPendingPreviews4Clamped to 1–16.
previewCooldownMilliseconds250Clamped to 50–60,000.

The session identity (CNA version, target platform, graphics renderer) is filled from CNA::getVersionString(), CNA::getCurrentPlatformName() and CNA::getCurrentGraphicsRendererName(). Agent::Start also has an overload taking an injected version-1 IDiagnosticsProvider (which must outlive the agent), which is how the tests drive it.

Run it end to end

These steps use the shipped demo, a small game that draws sprites, churns a texture every two seconds, and emits zones, markers and gauges. Tutorial 156 walks through them with more explanation.

cmake -S . -B build -DCNA_DIAGNOSTICS=FULL -DCNA_BUILD_INSPECTOR=ON     # CNA_BUILD_EXAMPLES is ON by default
cmake --build build --target cna_inspector_demo cna-inspector

build/cna_inspector_demo --port 47001 --seconds 60
#   prints:  Inspector port: 47001
#            Inspector token: <T>          (a secret; do not paste it into shared logs)

# in a second terminal
CNA_INSPECTOR_TOKEN='<T>' build/modules/inspector/cna-inspector --agent-port 47001
#   prints:  CNA Inspector is available at http://127.0.0.1:<port>/

Open the printed URL in a browser. The demo binary lands in the build root and the bridge in build/modules/inspector/ (no output-directory override is set). The demo accepts --seconds N (exit after N seconds; the default 0 runs until you close it) and --port P (default: an ephemeral port).

The cna-inspector bridge

OptionMeaning
--agent-port PORTRequired. The port the application printed.
--agent-host HOSTAgent host; default 127.0.0.1. A non-loopback host requires --allow-remote-agent.
--token TOKENThe agent token. Visible in the process list, so prefer one of the next two.
--token-file PATHRead the token from a local file (at most 256 bytes; trailing CR/LF stripped).
CNA_INSPECTOR_TOKENEnvironment variable used when neither --token nor --token-file is given.
--http-port PORTLocal browser port; default ephemeral.
--allow-remote-agentPermit an explicitly configured non-loopback agent.
--helpShow usage.

The bridge runs until you press Ctrl+C and always binds its HTTP server to 127.0.0.1; the browser never receives the agent’s authentication token.

Security model

⚠

Treat the token as a secret and keep the default loopback binding. The agent–bridge protocol is plaintext TCP, and the authentication token travels in its handshake. Do not enable allowRemote or --allow-remote-agent across an untrusted network; if you must inspect a remote machine, tunnel the agent port (for example over SSH) instead.

LayerControl
Agent exposureLoopback by default. A non-loopback bind fails to start with “non-loopback Inspector binding requires allowRemote=true”. One authenticated client and one request at a time; listen backlog of 4; handshake timeout 3 s and socket timeout 30 s; request-rate cap (default 64/s).
AuthenticationA 256-bit token from the OS secure random source, checked before the provider is ever queried (a test asserts a bad token gets no provider access).
Bridge HTTPAlways 127.0.0.1; at most 32 concurrent connections (excess refused); 16 KiB request cap and 12 MiB response cap; 5 s I/O timeout and a 1 s header timeout; the Host header must equal 127.0.0.1:<port>.
Browser pageThe bridge injects a per-process UI token into the page and requires it in the x-cna-inspector-ui-token header on every /api/ request. Responses carry Cache-Control: no-store, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and a strict same-origin Content-Security-Policy (default-src 'self', frame-ancestors 'none'); no CORS header is sent. All UI assets are embedded, with no CDN.
What it cannot doNothing beyond the protocol’s read messages and explicit, bounded resource-preview requests. Nothing writes to the game.

One consequence worth knowing: the UI token is embedded in the page served at /, so the trust boundary of the bridge is “this machine”, not “this user”. Anyone who can open http://127.0.0.1:<port>/ on the same host can read what the Inspector shows, and that is more than counters: the application name and metadata, metric, zone and event names, timings, resource labels, formats and sizes and, when a preview provider is installed, encoded pixels of a chosen texture or render target. Treat an exposed Inspector like a debug service. Run it on machines you control, for the time you need it.

The browser UI

The UI has eight views. It polls every 500 ms, pulls up to 512 events per round (up to four rounds), keeps 1,000 events locally, and shows an em dash for any metric the game does not publish.

ViewWhat it shows
SessionCNA version, target, platform backend, renderer, build, protocol and provider versions, local/remote exposure, your metadata and the negotiated capabilities.
PerformanceFrame rate, frame time and a frame graph from the 240-frame history, frame number, metric and resource counts, the profiler’s memory lower bound, and the metrics table with unit, kind and accuracy.
CPU profilerHot zones aggregated over the latest 1,000 retained events and a recent timeline (FULL only).
GraphicsDraw calls, primitives, texture-binding changes, resource count and declared resource bytes; it states plainly that there is no shader reflection.
ResourcesA sortable, filterable list (id, kind, label, dimensions, format, mips, bytes, accuracy), fetched only when the view opens or you press Refresh; a Preview button appears for texture and render-target rows when a preview provider exists.
AudioAudio/AllocatedVoices, Audio/VoiceCreations and audio resources; “allocated” is not necessarily “audible”.
InputAn explicit “unavailable” state: no input metrics are published.
EventsA filterable log: sequence, frame, thread, kind, category, name, duration and value.

A discontinuity banner reports events lost since the UI connected. Apart from user-initiated resource preview requests and metadata refresh, the UI controls nothing in the game.

Resource previews

A preview (a small image of a texture or render target) is an explicit, bounded request. The application supplies an IResourcePreviewProvider with RequestPreview and PollPreview; both must return promptly, start or poll asynchronous readback and never wait for a render device or queue. Only registered texture and render-target ids are eligible, previews are limited to 4 MiB of encoded data, and the requested size defaults to 1024×1024 with a protocol maximum of 4096×4096.

No renderer installs a preview provider at this snapshot, so the preview capability is absent in real sessions and the Preview buttons stay disabled. The seam exists for a future renderer-side implementation (or your own).

Wire protocol v1

For tool authors. Agent and bridge speak a compact, little-endian binary protocol over plaintext TCP; Client (in CNA/Inspector/Client.hpp) is a synchronous implementation with Connect, CaptureSnapshot, ReadEvents, RequestPreview, PollPreview, Ping and Disconnect.

ItemValue
FramingA fixed 24-byte header (magic CNAI, major 1, minor 0, message type, flags, payload length, request id) followed by the payload. Maximum payload 8 MiB.
MessagesClientHello 1, ServerHello 2, Error 3, SnapshotRequest 10, SnapshotResponse 11, EventsRequest 12, EventsResponse 13, PreviewRequest 14, PreviewPollRequest 15, PreviewResponse 16, Ping 17, Pong 18.
Capability bitsSnapshots, Events, ResourceMetadata, CpuZones, Accuracy, Discontinuities, ResourcePreviews (bits 0–6).
Error codesInvalidRequest, UnsupportedVersion, Unauthorized, Unavailable, LimitExceeded, Busy, InternalError.
BoundsAt most 4,096 events per response; at most 4 MiB of preview data; snapshots can be requested selectively (metrics, frames, resources).
Bridge JSON APIGET /api/session, GET /api/snapshot, GET /api/events, POST /api/preview, GET /api/preview; 64-bit integers are sent as JSON strings so JavaScript cannot lose precision.

Performance

What the code guarantees: linking the module without calling Agent::Start() costs nothing at run time; an idle agent does not call the provider. Because provider v1 returns an owned, complete snapshot, even an overview request copies the resource registry inside the process. CNA’s own recorded numbers (same Ryzen 7 PRO 7840U, GCC 14.2, Release machine as the Diagnostics figures; one machine, not reproduced by us) put the linked-but-not-started and idle-agent cases within noise of a compiled-out control, a live snapshot at about 71 µs per request and a 512-zone profiling response at about 121 µs per request on the agent thread, and a 1 MiB preview transport at about 1.4 ms excluding GPU readback. Reproduce with -DCNA_BUILD_BENCHMARKS=ON and the targets cna_inspector_benchmark and cna_inspector_compiled_out_benchmark.

Troubleshooting

SymptomLikely cause
cna-inspector or cna_inspector_demo target does not existConfigured without -DCNA_BUILD_INSPECTOR=ON; or the demo is missing because CNA_BUILD_EXAMPLES is off.
Configure stops with an Inspector platform errorThe target is Emscripten, Android or iOS; the Inspector supports desktop platforms only.
“--agent-port and an authentication token are required”Set --agent-port and provide the token through --token, --token-file or CNA_INSPECTOR_TOKEN.
Connection refusedThe game is not running, has not called Agent::Start(), or you used a different port than the one it printed.
Authentication failed (Unauthorized)The token does not match the agent’s current token. It is regenerated on every start unless you configured one.
“a non-loopback agent requires --allow-remote-agent”Deliberate: pass the flag only for a trusted network, or tunnel the port instead.
Session opens but Performance/CPU profiler are emptyThe game was built with CNA_DIAGNOSTICS=OFF; use STATS, or FULL for CPU zones and events.
No events or CPU zones under STATSExpected: events and zones are FULL-only.
Preview buttons are disabledExpected at this snapshot: no renderer supplies a preview provider.
“Reconnecting”The status light in the UI’s top bar reads “reconnecting” when an API request fails, for example because the game exited. The UI retries every 500 ms and negotiates a fresh session when the agent is back.

Status and limits

  • Tests: 27 test definitions in InspectorTests.cpp (protocol round-trips, mutation and truncation, frontend asset assertions, real-loopback agent tests including reconnect, rate back-pressure, preview bounds and non-loopback authorization). They compile only when CNA_BUILD_INSPECTOR=ON; the focused target is CnaInspectorTests. Static definitions, not pass results.
  • CI: no workflow builds the Inspector.
  • Validation: CNA’s own notes claim native-MSVC and Linux-plus-Chrome end-to-end validation and describe macOS as implemented with runtime validation pending. We have not reproduced any of it.
  • Data: only what the engine publishes to Diagnostics is visible; there are no GPU timings, input metrics or process-memory totals, and no renderer-side preview provider.

What the Inspector deliberately leaves out

The Inspector observes one process that opted in; it is not a management surface. Read at snapshot 009d40f5 from modules/inspector and CNA's own design note, these boundaries hold by construction rather than by configuration:

  • No discovery and no attach. Nothing in the module broadcasts, multicasts or scans: a bridge reaches an agent only through the host and port the application printed, and only after that application called Agent::Start(). There is no way to attach to an arbitrary process.
  • One client, plain TCP. The agent serves one authenticated connection at a time over plaintext TCP; there is no TLS and no multi-client fan-out. The token authenticates, it does not encrypt, which is why the guide recommends tunnelling rather than allowRemote.
  • No push channel. The browser page polls the bridge's HTTP API; the embedded assets contain no WebSocket (a frontend test, AssetsAreOfflineBoundedAndDemandDriven, asserts its absence), and the agent itself never builds JSON.
  • A token file is only bounded. --token-file reads at most 256 bytes and strips a trailing CR/LF; the bridge does not check the file's ownership or permissions, so keeping it owner-readable is the caller's job.
  • Not lossless. The Inspector shows what Diagnostics retained: bounded rings overwrite old events and full producer rings drop new ones, and the UI reports those losses instead of presenting a complete trace.

CNA's design note (docs/inspector.md) also lists what is deferred on purpose: renderer preview implementations, macOS runtime evidence, a mobile or device transport, TLS, multi-client fan-out, input-state metrics, universal effect or shader reflection, game-object plugins and offline trace comparison. It sets one rule for anything added later: a game-specific data extension must be a separately versioned, bounded, opt-in capability, and must not turn the Inspector into an editor or a generic object or memory browser. The protocol's "exactly 1.0 at both ends" version rule is what makes such an extension a deliberate, reviewable change (see the wire compatibility rules).