|
| 1 | +# Debug Logging |
| 2 | + |
| 3 | +Cellier v2 includes structured debug logging for the rendering pipeline. By |
| 4 | +default all loggers are silent (level `WARNING`). You opt in to the output you |
| 5 | +need — by category and by level — so there is zero noise until you ask for it |
| 6 | +and zero runtime cost in production. |
| 7 | + |
| 8 | +## Categories |
| 9 | + |
| 10 | +There are four independent loggers, each covering a different part of the |
| 11 | +pipeline: |
| 12 | + |
| 13 | +| Category | Logger name | What it covers | |
| 14 | +|----------|-------------------------|------------------------------------------------| |
| 15 | +| `perf` | `cellier.render.perf` | Planning timing, fetch latency statistics | |
| 16 | +| `gpu` | `cellier.render.gpu` | Brick/tile writes, LUT rebuilds | |
| 17 | +| `cache` | `cellier.render.cache` | Cache hit/miss summaries, eviction, clear | |
| 18 | +| `slicer` | `cellier.render.slicer` | Async task lifecycle, batch progress | |
| 19 | + |
| 20 | +All four live under the `cellier.render` parent logger, so standard Python |
| 21 | +logging hierarchy applies. |
| 22 | + |
| 23 | +## Log levels |
| 24 | + |
| 25 | +Each category uses three levels: |
| 26 | + |
| 27 | +| Level | What you see | |
| 28 | +|-----------|--------------------------------------------------------------| |
| 29 | +| `WARNING` | Anomalies only (e.g. cache budget exceeded). **Default.** | |
| 30 | +| `INFO` | Per-frame / per-batch summaries — the useful overview. | |
| 31 | +| `DEBUG` | Per-brick / per-tile detail — full firehose for deep debugging. | |
| 32 | + |
| 33 | +### What each level looks like in practice |
| 34 | + |
| 35 | +**`WARNING`** — only fires when something is wrong: |
| 36 | + |
| 37 | +``` |
| 38 | +PERF budget_exceeded required=500 budget=256 dropped=244 |
| 39 | +``` |
| 40 | + |
| 41 | +**`INFO`** — one or two lines per frame / per batch: |
| 42 | + |
| 43 | +``` |
| 44 | +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 |
| 45 | +PERF fetch_summary status=complete bricks=12 batches=2 total=58ms per_batch=28.3±5.1ms min=23.2ms max=33.4ms |
| 46 | +CACHE cache_state frame=7 occupied=130/512 free=382 hits=130 misses=12 evictions=0 |
| 47 | +SLICER batch_done 1/2 bricks=8 scales={0: 5, 1: 3} |
| 48 | +GPU gpu_flush bricks_in_batch=8 resident=138 |
| 49 | +``` |
| 50 | + |
| 51 | +**`DEBUG`** — per-brick detail inside each batch: |
| 52 | + |
| 53 | +``` |
| 54 | +PERF fetch_batch 1/2 bricks=8 elapsed=23.2ms |
| 55 | +SLICER brick_received id=abc123 scale=0 shape=(34, 34, 34) |
| 56 | +CACHE evict victim=BlockKey3D(level=1, gz=0, gy=2, gx=3) slot=42 |
| 57 | +GPU brick_written key=BlockKey3D(level=1, gz=0, gy=1, gx=2) slot=7 grid_pos=(0, 0, 7) |
| 58 | +``` |
| 59 | + |
| 60 | +## Quick start |
| 61 | + |
| 62 | +### Enable all categories at DEBUG (full output) |
| 63 | + |
| 64 | +```python |
| 65 | +from cellier.v2.logging import enable_debug_logging |
| 66 | + |
| 67 | +enable_debug_logging() |
| 68 | +``` |
| 69 | + |
| 70 | +### Enable specific categories |
| 71 | + |
| 72 | +```python |
| 73 | +enable_debug_logging(categories=("perf", "cache")) |
| 74 | +``` |
| 75 | + |
| 76 | +### Summaries only (INFO level) |
| 77 | + |
| 78 | +```python |
| 79 | +import logging |
| 80 | +from cellier.v2.logging import enable_debug_logging, _CATEGORY_MAP |
| 81 | + |
| 82 | +# Enable the handler infrastructure (sets everything to DEBUG). |
| 83 | +enable_debug_logging() |
| 84 | + |
| 85 | +# Override: set all categories to INFO so per-brick detail is suppressed. |
| 86 | +for logger in _CATEGORY_MAP.values(): |
| 87 | + logger.setLevel(logging.INFO) |
| 88 | +``` |
| 89 | + |
| 90 | +### Mixed levels |
| 91 | + |
| 92 | +```python |
| 93 | +import logging |
| 94 | +from cellier.v2.logging import enable_debug_logging, _CATEGORY_MAP |
| 95 | + |
| 96 | +enable_debug_logging(categories=("perf", "cache")) |
| 97 | + |
| 98 | +# Summaries for perf, full detail for cache. |
| 99 | +_CATEGORY_MAP["perf"].setLevel(logging.INFO) |
| 100 | +_CATEGORY_MAP["cache"].setLevel(logging.DEBUG) |
| 101 | +``` |
| 102 | + |
| 103 | +### Disable logging |
| 104 | + |
| 105 | +```python |
| 106 | +from cellier.v2.logging import disable_debug_logging |
| 107 | + |
| 108 | +disable_debug_logging() |
| 109 | +``` |
| 110 | + |
| 111 | +This resets all category loggers to `WARNING` and removes the handler. |
| 112 | + |
| 113 | +### Plain output (no Rich) |
| 114 | + |
| 115 | +```python |
| 116 | +enable_debug_logging(use_rich=False) |
| 117 | +``` |
| 118 | + |
| 119 | +Falls back to a plain `StreamHandler` with timestamps. Useful in CI or when |
| 120 | +Rich is not installed. |
| 121 | + |
| 122 | +## Rich colored output |
| 123 | + |
| 124 | +If the `rich` package is installed (`pip install cellier[logging]`), the |
| 125 | +default handler colors each category: |
| 126 | + |
| 127 | +| Category | Color | |
| 128 | +|----------|---------| |
| 129 | +| PERF | cyan | |
| 130 | +| GPU | green | |
| 131 | +| CACHE | yellow | |
| 132 | +| SLICER | magenta | |
| 133 | + |
| 134 | +If Rich is not available, output falls back to a plain `StreamHandler` |
| 135 | +automatically. |
| 136 | + |
| 137 | +## How it works under the hood |
| 138 | + |
| 139 | +All cellier loggers start at Python's default level (`WARNING`). |
| 140 | +`enable_debug_logging()` does two things: |
| 141 | + |
| 142 | +1. Sets the requested category loggers to `DEBUG`. |
| 143 | +2. Attaches a handler to the `cellier.render` parent logger (once only). |
| 144 | + |
| 145 | +Every log call in the instrumented code is either: |
| 146 | + |
| 147 | +- Emitted unconditionally (for `INFO` / `WARNING` calls, which are |
| 148 | + infrequent). |
| 149 | +- Guarded by `if logger.isEnabledFor(logging.DEBUG)` (for hot loops over |
| 150 | + bricks/tiles), so there is zero string formatting cost when DEBUG is off. |
| 151 | + |
| 152 | +Because the loggers are standard `logging.Logger` instances, you can |
| 153 | +integrate them into any existing logging configuration (file handlers, |
| 154 | +JSON formatters, external aggregation) using the logger names listed above. |
| 155 | + |
| 156 | +## Fetch latency timing |
| 157 | + |
| 158 | +The `perf` category includes fetch latency instrumentation for async data |
| 159 | +loading. This is especially useful when working with remote data stores |
| 160 | +where network I/O dominates. |
| 161 | + |
| 162 | +At **INFO**, a single summary line is emitted when a fetch task completes |
| 163 | +(or is cancelled): |
| 164 | + |
| 165 | +``` |
| 166 | +PERF fetch_summary status=complete bricks=96 batches=12 total=340ms per_batch=28.3±5.1ms min=19.0ms max=42.0ms |
| 167 | +``` |
| 168 | + |
| 169 | +- `total` — wall-clock time from first batch to last, including callback |
| 170 | + overhead (GPU commit, LUT rebuild, Qt yield) between batches. |
| 171 | +- `per_batch` — mean ± std of the I/O-only time for each `asyncio.gather()` |
| 172 | + call. This isolates the storage backend latency from the rendering overhead. |
| 173 | +- `min` / `max` — fastest and slowest batch, useful for spotting tail |
| 174 | + latency spikes. |
| 175 | + |
| 176 | +At **DEBUG**, each batch gets its own timing line as it completes: |
| 177 | + |
| 178 | +``` |
| 179 | +PERF fetch_batch 3/12 bricks=8 elapsed=31.2ms |
| 180 | +``` |
0 commit comments