Debug Logging¶
Cellier includes structured debug logging to aid developers in understanding what is going on under the hood. By
default all loggers are silent (level WARNING). You opt in to the output you
need — by category and by level — so there is zero noise until you ask for it
and zero runtime cost in production.
Categories¶
There are six independent loggers, each covering a different part of the pipeline:
| Category | Logger name | What it covers |
|---|---|---|
perf |
cellier.render.perf |
Planning timing, fetch latency statistics |
gpu |
cellier.render.gpu |
Brick/tile writes, LUT rebuilds |
cache |
cellier.render.cache |
Cache hit/miss summaries, eviction, clear |
slicer |
cellier.render.slicer |
Async task lifecycle, batch progress |
camera |
cellier.render.camera |
Camera change detection, settle timer, reslice trigger |
source_id |
cellier.render.source_id |
Source ID injection through the ContextVar → psygnal → bus bridge |
All six live under the cellier.render parent logger, so standard Python
logging hierarchy applies.
Log levels¶
Each category uses three levels:
| Level | What you see |
|---|---|
WARNING |
Anomalies only (e.g. cache budget exceeded). Default. |
INFO |
Per-frame / per-batch summaries — the useful overview. |
DEBUG |
Per-brick / per-tile detail — full firehose for deep debugging. |
What each level looks like in practice¶
WARNING — only fires when something is wrong:
INFO — one or two lines per frame / per batch:
PERF [frame 7] lod_select=1.2ms dist_sort=0.3ms frustum_cull=0.8ms stage=0.1ms | required=142 culled=40 hits=130 misses=12
PERF fetch_summary status=complete slice_id=d4e5... bricks=12 batches=2 total=58ms per_batch=28.3±5.1ms min=23.2ms max=33.4ms
CACHE cache_state frame=7 hot=130 reserve=8 free=374 hot_hits=120 reserve_hits=10 misses=12 evictions=0 pending_demote=0
SLICER batch_done 1/2 bricks=8 scales={0: 5, 1: 3}
GPU gpu_flush bricks_in_batch=8 resident=138
DEBUG — per-brick detail inside each batch:
PERF fetch_batch 1/2 bricks=8 elapsed=23.2ms
SLICER brick_received id=abc123 scale=0 shape=(34, 34, 34)
CACHE evict_hot victim=BlockKey3D(level=1, gz=0, gy=2, gx=3) slot=42
GPU brick_written key=BlockKey3D(level=1, gz=0, gy=1, gx=2) slot=7 grid_pos=(0, 0, 7)
The cache examples above are from the 3D (hot/reserve) tile manager. The 2D
tile manager emits a simpler cache_state frame=… occupied=…/… free=…
hits=… misses=… evictions=… line and a plain evict (no evict_hot).
Quick start¶
Enable all categories at DEBUG (full output)¶
Enable specific categories¶
Summaries only (INFO level)¶
Pass level to set the minimum level for every enabled category. INFO
suppresses the per-brick / per-tile detail and keeps the per-frame and
per-batch summaries:
import logging
from cellier.logging import enable_debug_logging
enable_debug_logging(level=logging.INFO)
Mixed levels¶
enable_debug_logging applies a single level to all the categories it
enables. To run different categories at different levels, enable the most
verbose set first: the shared handler is created on the first call and keeps
that call's level, so the most verbose level must be installed up front or
the handler will filter the finer records out.
import logging
from cellier.logging import enable_debug_logging
# Full detail for cache (installs the handler at DEBUG), summaries for perf.
enable_debug_logging(categories=("cache",), level=logging.DEBUG)
enable_debug_logging(categories=("perf",), level=logging.INFO)
Disable logging¶
This resets all category loggers to WARNING and removes the handler.
Plain output (no Rich)¶
Falls back to a plain StreamHandler with timestamps. Useful in CI or when
Rich is not installed.
Rich colored output¶
If the rich package is installed (pip install cellier[logging]), the
default handler colors each category:
| Category | Color |
|---|---|
| PERF | cyan |
| GPU | green |
| CACHE | yellow |
| SLICER | magenta |
| CAMERA | blue |
| SOURCE_ID | red |
If Rich is not available, output falls back to a plain StreamHandler
automatically.
Event source ID logging¶
The source_id category traces how source_id is injected into bus events.
It is useful when debugging echo-filtering issues or unexpected widget updates.
See debugging_events for a full walkthrough.
Enable it with:
Each model mutation through a controller method produces three lines:
[SOURCE_ID] set field=clim visual=9c1b... source=3f2a...
[SOURCE_ID] bridge handler=_on_appearance_psygnal visual=9c1b... field=clim resolved_source=3f2a... override_active=True
[SOURCE_ID] reset field=clim visual=9c1b...
set— a controller mutation method (update_appearance_field,update_slice_indices, orupdate_aabb_field) was called and theContextVarwas set.bridge— the psygnal handler fired and read theContextVar.override_active=Trueconfirms the source came from a controller method.override_active=Falsemeans the model field was mutated directly, andsource_idfell back to the controller's own ID.reset— theContextVarwas restored after the mutation completed.
How it works under the hood¶
All cellier loggers start at Python's default level (WARNING).
enable_debug_logging() does two things:
- Sets the requested category loggers to the requested
level(DEBUGby default). - Attaches a handler to the
cellier.renderparent logger (once only), at that same level.
Every log call in the instrumented code is either:
- Emitted unconditionally (for
INFO/WARNINGcalls, which are infrequent). - Guarded by
if logger.isEnabledFor(logging.DEBUG)(for hot loops over bricks/tiles), so there is zero string formatting cost when DEBUG is off.
Because the loggers are standard logging.Logger instances, you can
integrate them into any existing logging configuration (file handlers,
JSON formatters, external aggregation) using the logger names listed above.
Fetch latency timing¶
The perf category includes fetch latency instrumentation for async data
loading. This is especially useful when working with remote data stores
where network I/O dominates.
At INFO, a single summary line is emitted when a fetch task completes (or is cancelled):
PERF fetch_summary status=complete slice_id=d4e5... bricks=96 batches=12 total=340ms per_batch=28.3±5.1ms min=19.0ms max=42.0ms
total— wall-clock time from first batch to last, including callback overhead (GPU commit, LUT rebuild, Qt yield) between batches.per_batch— mean ± std of the I/O-only time for eachasyncio.gather()call. This isolates the storage backend latency from the rendering overhead.min/max— fastest and slowest batch, useful for spotting tail latency spikes.
At DEBUG, each batch gets its own timing line as it completes: