Skip to content

Commit b16cc21

Browse files
authored
Merge pull request #29 from cBioPortal/codex/beta-16m-contract
perf(wsi): unify 16M decode and metadata contract
2 parents dd3ec52 + e00de90 commit b16cc21

14 files changed

Lines changed: 497 additions & 62 deletions

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ optional Redis cache:
100100
| `THUMBNAIL_PREWARM_URI` | — | Optional stable thumbnail object used to prewarm each worker |
101101
| `TILE_SIZE` | `256` | Tile edge length |
102102
| `JPEG_QUALITY` | `85` | JPEG encoding quality |
103-
| `MAX_DECODE_PIXELS` | `4194304` | Maximum on-demand tile decode |
103+
| `MAX_DECODE_PIXELS` | `16777216` | Maximum on-demand tile decode |
104+
| `THUMBNAIL_MAX_DECODE_PIXELS` | `16777216` | Maximum thumbnail decode |
104105
| `MAX_OPEN_SLIDES` | `64` | Open-slide LRU capacity |
105106
| `MAX_IMAGE_OPERATIONS` | `2` | Concurrent pixel operations per worker |
106107
| `N_WORKERS` | `4` | Gunicorn worker count |

‎app/config.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ class Settings:
7272
# Tile settings
7373
tile_size: int = field(default_factory=lambda: _env_int("TILE_SIZE", 256))
7474
jpeg_quality: int = field(default_factory=lambda: _env_int("JPEG_QUALITY", 85))
75-
max_decode_pixels: int = field(default_factory=lambda: _env_int("MAX_DECODE_PIXELS", 4_194_304))
75+
max_decode_pixels: int = field(default_factory=lambda: _env_int("MAX_DECODE_PIXELS", 16_777_216))
7676
thumbnail_max_decode_pixels: int = field(
77-
default_factory=lambda: _env_int("THUMBNAIL_MAX_DECODE_PIXELS", 16_000_000)
77+
default_factory=lambda: _env_int("THUMBNAIL_MAX_DECODE_PIXELS", 16_777_216)
7878
)
7979
thumbnail_timeout_sec: int = field(
8080
default_factory=lambda: _env_int("THUMBNAIL_TIMEOUT_SEC", 8)

‎app/identity.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010
IDENTITY_VERSION = "v2"
11+
TILE_METADATA_SCHEMA_VERSION = 2
1112

1213

1314
def timestamp_to_epoch_ms(value: Any) -> int | None:
@@ -39,7 +40,7 @@ def source_fingerprint(path: Any, size: Any, last_modified: Any) -> str | None:
3940

4041

4142
def decode_policy_version() -> str:
42-
return f"geometry-v1;tile-max={settings.max_decode_pixels};thumbnail-max={settings.thumbnail_max_decode_pixels}"
43+
return f"geometry-v2;tile-max={settings.max_decode_pixels};thumbnail-max={settings.thumbnail_max_decode_pixels}"
4344

4445

4546
from .config import settings # noqa: E402 (settings is needed by policy helper)

‎app/metadata_contract.py‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Validation for the source-bound WSI tile metadata contract."""
2+
from __future__ import annotations
3+
4+
import math
5+
from typing import Any
6+
7+
from .config import settings
8+
from .identity import TILE_METADATA_SCHEMA_VERSION, decode_policy_version
9+
10+
11+
def validate_tile_metadata(
12+
metadata: Any,
13+
*,
14+
allow_legacy: bool = True,
15+
) -> tuple[bool, str | None]:
16+
"""Return whether metadata is usable by the tile server.
17+
18+
Schema-null records are retained for the migration window, but an
19+
explicitly versioned record must satisfy the complete v2 contract.
20+
"""
21+
if not isinstance(metadata, dict):
22+
return False, "metadata_not_object"
23+
24+
schema = metadata.get("tile_metadata_schema_version")
25+
if schema is None:
26+
if not allow_legacy:
27+
return False, "legacy_metadata_not_allowed"
28+
return _validate_common(metadata, require_v2=False)
29+
if (
30+
not isinstance(schema, int)
31+
or isinstance(schema, bool)
32+
or schema != TILE_METADATA_SCHEMA_VERSION
33+
):
34+
return False, "unsupported_metadata_schema"
35+
36+
valid, reason = _validate_common(metadata, require_v2=True)
37+
if not valid:
38+
return valid, reason
39+
if metadata.get("safe_min_level") is None:
40+
return False, "missing_safe_min_level"
41+
if metadata.get("decode_policy_version") != decode_policy_version():
42+
return False, "stale_decode_policy"
43+
if (
44+
not isinstance(metadata.get("max_decode_pixels"), int)
45+
or isinstance(metadata.get("max_decode_pixels"), bool)
46+
or metadata.get("max_decode_pixels") != settings.max_decode_pixels
47+
):
48+
return False, "stale_tile_decode_limit"
49+
if (
50+
not isinstance(metadata.get("thumbnail_max_decode_pixels"), int)
51+
or isinstance(metadata.get("thumbnail_max_decode_pixels"), bool)
52+
or metadata.get("thumbnail_max_decode_pixels") != settings.thumbnail_max_decode_pixels
53+
):
54+
return False, "stale_thumbnail_decode_limit"
55+
return True, None
56+
57+
58+
def _validate_common(metadata: dict[str, Any], *, require_v2: bool) -> tuple[bool, str | None]:
59+
dimensions = metadata.get("dimensions")
60+
if not isinstance(dimensions, dict):
61+
return False, "invalid_dimensions"
62+
width = dimensions.get("width")
63+
height = dimensions.get("height")
64+
if not _positive_int(width) or not _positive_int(height):
65+
return False, "invalid_dimensions"
66+
67+
levels = metadata.get("levels")
68+
if not _positive_int(levels):
69+
return False, "invalid_levels"
70+
level_dimensions = metadata.get("level_dimensions")
71+
if not isinstance(level_dimensions, list) or len(level_dimensions) != levels:
72+
return False, "invalid_level_dimensions"
73+
for level in level_dimensions:
74+
if (
75+
not isinstance(level, dict)
76+
or not _positive_int(level.get("width"))
77+
or not _positive_int(level.get("height"))
78+
):
79+
return False, "invalid_level_dimensions"
80+
81+
downsample_values = metadata.get("level_downsamples")
82+
if require_v2:
83+
if not isinstance(downsample_values, list) or len(downsample_values) != levels:
84+
return False, "invalid_level_downsamples"
85+
if any(
86+
not isinstance(value, (int, float))
87+
or isinstance(value, bool)
88+
or not math.isfinite(float(value))
89+
or float(value) <= 0
90+
for value in downsample_values
91+
):
92+
return False, "invalid_level_downsamples"
93+
94+
max_zoom = metadata.get("max_zoom")
95+
tile_size = metadata.get("tile_size")
96+
if not isinstance(max_zoom, int) or isinstance(max_zoom, bool) or max_zoom < 0:
97+
return False, "invalid_max_zoom"
98+
if not _positive_int(tile_size):
99+
return False, "invalid_tile_size"
100+
101+
safe_min_level = metadata.get("safe_min_level")
102+
if safe_min_level is not None and (
103+
not isinstance(safe_min_level, int)
104+
or isinstance(safe_min_level, bool)
105+
or safe_min_level < 0
106+
or safe_min_level > max_zoom
107+
):
108+
return False, "invalid_safe_min_level"
109+
return True, None
110+
111+
112+
def _positive_int(value: Any) -> bool:
113+
return isinstance(value, int) and not isinstance(value, bool) and value > 0

‎app/metrics.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
DECODE_SOURCE_PIXELS = Histogram(
1717
"tile_server_decode_source_pixels",
1818
"Source pixels decoded for a tile or thumbnail request.",
19-
buckets=(0, 65_536, 262_144, 1_048_576, 4_194_304, 8_388_608, float("inf")),
19+
buckets=(0, 65_536, 262_144, 1_048_576, 4_194_304, 8_388_608, 16_777_216, float("inf")),
2020
)
2121
OVERSIZED_DECODE_REJECTIONS = Counter(
2222
"tile_server_oversized_decode_rejections_total",

‎app/tiles.py‎

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@
1717
from tiffslide import TiffSlide
1818

1919
from .config import settings
20+
from .identity import IDENTITY_VERSION, TILE_METADATA_SCHEMA_VERSION, decode_policy_version
2021
from .metrics import DECODE_SOURCE_PIXELS
2122

2223
TILE_SIZE = settings.tile_size
23-
DECODE_POLICY_VERSION = (
24-
f"geometry-v1;tile-max={settings.max_decode_pixels};"
25-
f"thumbnail-max={settings.thumbnail_max_decode_pixels}"
26-
)
24+
DECODE_POLICY_VERSION = decode_policy_version()
2725

2826

2927
class OverviewTooLarge(RuntimeError):
@@ -115,7 +113,65 @@ def max_zoom(slide: TiffSlide) -> int:
115113
rounding to the nearest power-of-two pyramid level).
116114
"""
117115
w, h = slide.dimensions
118-
return math.ceil(math.log2(max(w, h) / TILE_SIZE))
116+
return max(0, math.ceil(math.log2(max(w, h) / TILE_SIZE)))
117+
118+
119+
def _best_level_for_downsample(level_downsamples: list[float], downsample: float) -> int:
120+
"""Match tiffslide's level selection without opening or decoding a slide."""
121+
if downsample <= 1.0:
122+
return 0
123+
for level, level_downsample in enumerate(level_downsamples):
124+
if level_downsample > downsample:
125+
return max(0, level - 1)
126+
return len(level_downsamples) - 1
127+
128+
129+
def safe_min_level_from_geometry(
130+
*,
131+
width: int,
132+
height: int,
133+
level_dimensions: list[tuple[int, int]],
134+
level_downsamples: list[float],
135+
tile_size: int,
136+
max_decode_pixels: int,
137+
) -> int | None:
138+
"""Compute the first safe ZXY level using only intrinsic pyramid geometry."""
139+
if (
140+
width <= 0
141+
or height <= 0
142+
or not level_dimensions
143+
or len(level_dimensions) != len(level_downsamples)
144+
or tile_size <= 0
145+
or max_decode_pixels <= 0
146+
):
147+
return None
148+
pyramid_max_zoom = max(0, math.ceil(math.log2(max(width, height) / tile_size)))
149+
for z in range(pyramid_max_zoom + 1):
150+
target_ds = 2 ** (pyramid_max_zoom - z)
151+
tiles_x = max(1, math.ceil(width / (tile_size * target_ds)))
152+
tiles_y = max(1, math.ceil(height / (tile_size * target_ds)))
153+
worst_pixels = 0
154+
best_level = _best_level_for_downsample(level_downsamples, target_ds)
155+
level_w, level_h = level_dimensions[best_level]
156+
level_ds = max(1.0, float(level_downsamples[best_level]))
157+
for x in {0, tiles_x - 1}:
158+
for y in {0, tiles_y - 1}:
159+
x0 = x * tile_size * target_ds
160+
y0 = y * tile_size * target_ds
161+
src_w = min(tile_size * target_ds, width - x0)
162+
src_h = min(tile_size * target_ds, height - y0)
163+
read_w = min(
164+
math.ceil(src_w / level_ds),
165+
level_w - math.floor(x0 / level_ds),
166+
)
167+
read_h = min(
168+
math.ceil(src_h / level_ds),
169+
level_h - math.floor(y0 / level_ds),
170+
)
171+
worst_pixels = max(worst_pixels, max(0, read_w) * max(0, read_h))
172+
if worst_pixels <= max_decode_pixels:
173+
return z
174+
return None
119175

120176

121177
def safe_min_level(slide: TiffSlide) -> int | None:
@@ -125,33 +181,17 @@ def safe_min_level(slide: TiffSlide) -> int | None:
125181
result is emitted into offline tile metadata and remains advisory because
126182
the request path independently enforces the same pixel budget.
127183
"""
128-
slide_w, slide_h = slide.dimensions
129-
for z in range(max_zoom(slide) + 1):
130-
target_ds = 2 ** (max_zoom(slide) - z)
131-
tiles_x = max(1, math.ceil(slide_w / (TILE_SIZE * target_ds)))
132-
tiles_y = max(1, math.ceil(slide_h / (TILE_SIZE * target_ds)))
133-
worst_pixels = 0
134-
for x in {0, tiles_x - 1}:
135-
for y in {0, tiles_y - 1}:
136-
try:
137-
_, _, x0, y0, src_w, src_h, _, _ = _tile_geometry(slide, z, x, y)
138-
best_level = slide.get_best_level_for_downsample(target_ds)
139-
level_ds = max(1.0, float(slide.level_downsamples[best_level]))
140-
level_w, level_h = slide.level_dimensions[best_level]
141-
read_w = min(
142-
math.ceil(src_w / level_ds),
143-
level_w - math.floor(x0 / level_ds),
144-
)
145-
read_h = min(
146-
math.ceil(src_h / level_ds),
147-
level_h - math.floor(y0 / level_ds),
148-
)
149-
worst_pixels = max(worst_pixels, max(0, read_w) * max(0, read_h))
150-
except (IndexError, ValueError, TypeError):
151-
return None
152-
if worst_pixels <= settings.max_decode_pixels:
153-
return z
154-
return None
184+
try:
185+
return safe_min_level_from_geometry(
186+
width=int(slide.dimensions[0]),
187+
height=int(slide.dimensions[1]),
188+
level_dimensions=[(int(width), int(height)) for width, height in slide.level_dimensions],
189+
level_downsamples=[float(value) for value in slide.level_downsamples],
190+
tile_size=TILE_SIZE,
191+
max_decode_pixels=settings.max_decode_pixels,
192+
)
193+
except (IndexError, ValueError, TypeError, ZeroDivisionError):
194+
return None
155195

156196

157197
def _slide_properties_metadata(slide: TiffSlide) -> tuple[float, float, str, int | None]:
@@ -187,7 +227,8 @@ def slide_metadata(slide: TiffSlide) -> dict:
187227
"objective_power": objective_power,
188228
"vendor": vendor,
189229
"safe_min_level": safe_min_level(slide),
190-
"identity_version": "v2",
230+
"identity_version": IDENTITY_VERSION,
231+
"tile_metadata_schema_version": TILE_METADATA_SCHEMA_VERSION,
191232
"decode_policy_version": DECODE_POLICY_VERSION,
192233
"max_decode_pixels": settings.max_decode_pixels,
193234
"thumbnail_max_decode_pixels": settings.thumbnail_max_decode_pixels,

‎docker-compose.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ services:
3434
# ── Tile settings ─────────────────────────────────────────────────────
3535
TILE_SIZE: ${TILE_SIZE:-256}
3636
JPEG_QUALITY: ${JPEG_QUALITY:-85}
37-
MAX_DECODE_PIXELS: ${MAX_DECODE_PIXELS:-4194304}
37+
MAX_DECODE_PIXELS: ${MAX_DECODE_PIXELS:-16777216}
38+
THUMBNAIL_MAX_DECODE_PIXELS: ${THUMBNAIL_MAX_DECODE_PIXELS:-16777216}
3839

3940
# ── Redis cache ───────────────────────────────────────────────────────
4041
REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD must be set}@redis:6379

‎tests/test_config.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ def test_profile_used_when_env_absent(self):
6565
class TestOtherSettings:
6666
def test_max_decode_pixels_default(self):
6767
s = make_settings()
68-
assert s.max_decode_pixels == 4_194_304
68+
assert s.max_decode_pixels == 16_777_216
6969

7070
def test_thumbnail_max_decode_pixels_default(self):
7171
s = make_settings()
72-
assert s.thumbnail_max_decode_pixels == 16_000_000
72+
assert s.thumbnail_max_decode_pixels == 16_777_216
7373

7474
def test_thumbnail_cache_ttl_default(self):
7575
s = make_settings()

0 commit comments

Comments
 (0)