Skip to content

Commit 3ce3347

Browse files
Add logging (#58)
* add logging * delete old docs
1 parent 1496f71 commit 3ce3347

13 files changed

Lines changed: 693 additions & 2637 deletions

‎docs/cellier_controller_phase1_plan.md‎

Lines changed: 0 additions & 1237 deletions
This file was deleted.

‎docs/cellier_v2_eventbus_implementation_plan.md‎

Lines changed: 0 additions & 609 deletions
This file was deleted.

‎docs/cellier_v2_render_architecture_plan.md‎

Lines changed: 0 additions & 775 deletions
This file was deleted.

‎docs/logging.md‎

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
```

‎pyproject.toml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,14 @@ dev = [
6868
"ruff",
6969
]
7070

71+
# optional rich-powered debug logging
72+
logging = ["rich>=13.0"]
73+
7174
# dependencies for the example scripts
7275
examples = ["scikit-image"]
7376

7477
# get all dependencies for development
75-
dev-all = ["cellier[dev,test,pyqt,examples]"]
78+
dev-all = ["cellier[dev,test,pyqt,examples,logging]"]
7679

7780
[project.urls]
7881
homepage = "https://github.com/kevinyamauchi/cellier"

‎scripts/v2/integration_2d_3d/example_combined.py‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,72 @@
6464
from cellier.v2.scene.dims import CoordinateSystem
6565
from cellier.v2.visuals._image import ImageAppearance
6666

67+
# ---------------------------------------------------------------------------
68+
# Debug-logging CLI helper (power-user per-category:level syntax)
69+
# ---------------------------------------------------------------------------
70+
71+
72+
def _setup_debug_logging(spec: str) -> None:
73+
"""Parse ``--debug-log`` spec and configure cellier loggers.
74+
75+
Supports several forms::
76+
77+
"all" -> all categories at DEBUG
78+
"all:info" -> all categories at INFO
79+
"perf,cache" -> perf+cache at DEBUG
80+
"perf:info,cache:debug" -> per-category levels
81+
82+
A bare category name (no colon) defaults to DEBUG.
83+
"""
84+
import logging as _logging
85+
86+
from cellier.v2.logging import _CATEGORY_MAP, enable_debug_logging
87+
88+
_LEVEL_NAMES = {
89+
"debug": _logging.DEBUG,
90+
"info": _logging.INFO,
91+
"warning": _logging.WARNING,
92+
}
93+
94+
# Parse spec into {category: level_int} pairs.
95+
overrides: dict[str, int] = {}
96+
all_cats = tuple(_CATEGORY_MAP.keys())
97+
default_level = _logging.DEBUG
98+
99+
if spec in ("", "all"):
100+
# All categories at DEBUG — no overrides needed.
101+
pass
102+
elif spec.startswith("all:"):
103+
level_str = spec.split(":", 1)[1].strip().lower()
104+
default_level = _LEVEL_NAMES.get(level_str, _logging.DEBUG)
105+
else:
106+
for token in spec.split(","):
107+
token = token.strip()
108+
if ":" in token:
109+
cat, level_str = token.split(":", 1)
110+
overrides[cat.strip()] = _LEVEL_NAMES.get(
111+
level_str.strip().lower(), _logging.DEBUG
112+
)
113+
else:
114+
overrides[token] = _logging.DEBUG
115+
116+
# Determine which categories to enable.
117+
if overrides:
118+
cats = tuple(overrides.keys())
119+
else:
120+
cats = all_cats
121+
122+
# Step 1: enable handler + set all requested categories to DEBUG.
123+
enable_debug_logging(categories=cats)
124+
125+
# Step 2: apply per-category level overrides (or global default).
126+
for cat in cats:
127+
target_level = overrides.get(cat, default_level)
128+
logger = _CATEGORY_MAP.get(cat)
129+
if logger is not None:
130+
logger.setLevel(target_level)
131+
132+
67133
# ---------------------------------------------------------------------------
68134
# Constants
69135
# ---------------------------------------------------------------------------
@@ -563,6 +629,24 @@ def main() -> None:
563629
default=ZARR_PATH,
564630
help="Path to the multiscale zarr store.",
565631
)
632+
parser.add_argument(
633+
"--debug-log",
634+
metavar="SPEC",
635+
default=None,
636+
const="all",
637+
nargs="?",
638+
help=(
639+
"Enable debug logging. Comma-separated list of categories "
640+
"(perf, gpu, cache, slicer). "
641+
"Omit value or use 'all' for all categories at DEBUG level. "
642+
"Power-user: append :LEVEL to set per-category levels. "
643+
"Examples:\n"
644+
" --debug-log (all at DEBUG)\n"
645+
" --debug-log perf,cache (perf+cache at DEBUG)\n"
646+
" --debug-log all:info (all at INFO — summaries only)\n"
647+
" --debug-log perf:info,cache:debug,slicer:info"
648+
),
649+
)
566650
args = parser.parse_args()
567651

568652
if not args.zarr_path.exists():
@@ -571,6 +655,9 @@ def main() -> None:
571655
print(" uv run example.py --make-files")
572656
sys.exit(1)
573657

658+
if args.debug_log is not None:
659+
_setup_debug_logging(args.debug_log)
660+
574661
print("Opening tensorstore stores via MultiscaleZarrDataStore ...")
575662
data_store = MultiscaleZarrDataStore(
576663
zarr_path=str(args.zarr_path),
@@ -580,6 +667,9 @@ def main() -> None:
580667
for i, shape in enumerate(data_store.level_shapes):
581668
print(f" s{i}: shape={shape}")
582669
print()
670+
print(
671+
"Debug logging: --debug-log [SPEC] " "(e.g. all:info, perf:info,cache:debug)"
672+
)
583673
print("Press 'Update' to render. " "Toggle between 2D and 3D with the button.\n")
584674

585675
_app = QApplication([sys.argv[0]])

0 commit comments

Comments
 (0)