andandandand commited on
Commit
d110c29
·
verified ·
1 Parent(s): 62f2d56

Deploy cold-start reliability update (source: 85cf4fa)

Browse files

Batch ingestion + serve-fast warmup + runtime healthcheck tuning

Files changed (4) hide show
  1. .gitignore +2 -0
  2. Dockerfile +2 -2
  3. README.md +31 -0
  4. demo.py +345 -21
.gitignore CHANGED
@@ -1,3 +1,5 @@
 
 
1
  dataset_build/
2
  dataset_build_smoke/
3
  assets_runtime_smoke/
 
1
+ assets/*
2
+ !assets/.gitkeep
3
  dataset_build/
4
  dataset_build_smoke/
5
  assets_runtime_smoke/
Dockerfile CHANGED
@@ -32,7 +32,7 @@ COPY --chown=user assets ./assets
32
 
33
  EXPOSE 7860
34
 
35
- HEALTHCHECK --interval=30s --timeout=10s --start-period=600s --retries=3 \
36
- CMD curl -f http://localhost:7860/__hyperview__/health || exit 1
37
 
38
  CMD ["python", "demo.py"]
 
32
 
33
  EXPOSE 7860
34
 
35
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=10 \
36
+ CMD sh -c "curl -f http://127.0.0.1:${PORT:-7860}/__hyperview__/health || exit 1"
37
 
38
  CMD ["python", "demo.py"]
README.md CHANGED
@@ -27,6 +27,10 @@ Runtime environment variables:
27
  - `HF_DATASET_SPLIT` (default: `train`)
28
  - `EMBEDDING_ASSET_DIR` (default: `./assets`)
29
  - `EMBEDDING_ASSET_MANIFEST` (default: `${EMBEDDING_ASSET_DIR}/manifest.json`)
 
 
 
 
30
  - `HYPERVIEW_DEFAULT_PANEL` (default: `spherical3d`; enables Sphere 3D as initial scatter panel)
31
  - `HYPERVIEW_LAYOUT_CACHE_VERSION` (default: `v6`; bumps dock layout localStorage key to invalidate stale cached panel state)
32
  - `HYPERVIEW_BIND_HOST` (preferred bind host; optional)
@@ -40,6 +44,33 @@ On Hugging Face Spaces, `SPACE_HOST` may be injected as `<space-subdomain>.hf.sp
40
 
41
  The runtime also patches HyperView's dock-layout cache key from legacy `hyperview:dockview-layout:v5` to `hyperview:dockview-layout:${HYPERVIEW_LAYOUT_CACHE_VERSION}` to force migration away from stale panel layouts after UI/layout changes. For future migrations, increment `HYPERVIEW_LAYOUT_CACHE_VERSION` (for example, `v7`) without changing code.
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  ## Important Note
44
 
45
  HyperView similarity search currently uses cosine distance in storage backends. The Lorentz panel in this Space is intended for embedding-space visualization and geometry-aware comparison rather than canonical Lorentz-distance retrieval scoring.
 
27
  - `HF_DATASET_SPLIT` (default: `train`)
28
  - `EMBEDDING_ASSET_DIR` (default: `./assets`)
29
  - `EMBEDDING_ASSET_MANIFEST` (default: `${EMBEDDING_ASSET_DIR}/manifest.json`)
30
+ - `HYPERVIEW_STARTUP_MODE` (default: `serve_fast`; choices: `serve_fast|blocking`)
31
+ - `HYPERVIEW_WARMUP_STATUS_PATH` (default: `/tmp/hyperview_warmup_status.json`)
32
+ - `HYPERVIEW_WARMUP_FAILURE_POLICY` (default: `exit`; choices: `exit|warn`)
33
+ - `HYPERVIEW_BATCH_INSERT_SIZE` (default: `500`; controls sample-batch insertion chunk size)
34
  - `HYPERVIEW_DEFAULT_PANEL` (default: `spherical3d`; enables Sphere 3D as initial scatter panel)
35
  - `HYPERVIEW_LAYOUT_CACHE_VERSION` (default: `v6`; bumps dock layout localStorage key to invalidate stale cached panel state)
36
  - `HYPERVIEW_BIND_HOST` (preferred bind host; optional)
 
44
 
45
  The runtime also patches HyperView's dock-layout cache key from legacy `hyperview:dockview-layout:v5` to `hyperview:dockview-layout:${HYPERVIEW_LAYOUT_CACHE_VERSION}` to force migration away from stale panel layouts after UI/layout changes. For future migrations, increment `HYPERVIEW_LAYOUT_CACHE_VERSION` (for example, `v7`) without changing code.
46
 
47
+ ## Startup and Warmup Semantics
48
+
49
+ - `HYPERVIEW_STARTUP_MODE=serve_fast` (default):
50
+ - Starts the HyperView server immediately.
51
+ - Runs dataset warmup asynchronously in a background thread.
52
+ - Warmup phases are persisted as JSON: `ingest -> spaces -> layouts -> ready`.
53
+ - `HYPERVIEW_STARTUP_MODE=blocking`:
54
+ - Performs warmup synchronously before serving traffic.
55
+
56
+ Warmup status JSON fields include:
57
+
58
+ - `status` (`starting|running|ready|failed`)
59
+ - `phase` (`boot|ingest|spaces|layouts|ready|failed`)
60
+ - `counts` (sample/space/layout counters and ingestion stats)
61
+ - `error` (exception payload when warmup fails)
62
+ - `timestamps` (`started_at`, `updated_at`, plus terminal timestamps)
63
+
64
+ Failure policy behavior:
65
+
66
+ - `HYPERVIEW_WARMUP_FAILURE_POLICY=exit` (default): process exits on warmup failure.
67
+ - `HYPERVIEW_WARMUP_FAILURE_POLICY=warn`: process stays up and records failure in warmup status JSON.
68
+
69
+ Healthcheck semantics:
70
+
71
+ - Container health (`/__hyperview__/health`) indicates server liveness only.
72
+ - Data readiness (dataset/spaces/layouts completed) is indicated by warmup status JSON (`status=ready`).
73
+
74
  ## Important Note
75
 
76
  HyperView similarity search currently uses cosine distance in storage backends. The Lorentz panel in this Space is intended for embedding-space visualization and geometry-aware comparison rather than canonical Lorentz-distance retrieval scoring.
demo.py CHANGED
@@ -6,6 +6,10 @@ from __future__ import annotations
6
  import json
7
  import os
8
  import re
 
 
 
 
9
  from pathlib import Path
10
  from typing import Any
11
 
@@ -14,6 +18,7 @@ from datasets import Dataset as HFDataset
14
  from datasets import DatasetDict as HFDatasetDict
15
  from datasets import load_dataset, load_from_disk
16
  import hyperview as hv
 
17
  from hyperview.core.sample import Sample
18
 
19
  SPACE_HOST = os.environ.get("SPACE_HOST", "0.0.0.0")
@@ -34,6 +39,113 @@ ASSET_MANIFEST_PATH = Path(
34
  os.environ.get("EMBEDDING_ASSET_MANIFEST", str((EMBEDDING_ASSET_DIR / "manifest.json").resolve()))
35
  )
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  def _patch_hyperview_default_panel() -> None:
39
  """Patch HyperView 0.3.1 frontend for default panel and dock cache-key migration.
@@ -169,45 +281,103 @@ def _load_hf_rows() -> HFDataset:
169
  return load_dataset(HF_DATASET_REPO, name=HF_DATASET_CONFIG, split=HF_DATASET_SPLIT)
170
 
171
 
172
- def ingest_hf_dataset_samples(dataset: hv.Dataset) -> None:
173
  rows = _load_hf_rows()
174
  media_root = Path(os.environ.get("HYPERVIEW_MEDIA_DIR", "./demo_data/media")) / DATASET_NAME
175
  media_root.mkdir(parents=True, exist_ok=True)
176
 
177
- added = 0
 
 
 
 
 
178
  for index, row in enumerate(rows):
179
  filename = str(row.get("filename", f"sample_{index:06d}.jpg"))
180
  sample_id = str(row.get("sample_id", filename))
181
- if dataset._storage.get_sample(sample_id) is not None:
 
182
  continue
 
 
 
 
 
 
 
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  image_obj = row["image"]
185
  image_path = media_root / f"{Path(sample_id).stem}.jpg"
186
  if not image_path.exists():
187
  image_obj.convert("RGB").save(image_path, format="JPEG", quality=90, optimize=True)
 
188
 
189
- label = str(row.get("label", ""))
190
  metadata = {
191
  "filename": filename,
192
  "sample_id": sample_id,
193
- "split_tag": str(row.get("split_tag", "unknown")),
194
- "identity": label,
195
  "source_repo": HF_DATASET_REPO,
196
  "source_config": HF_DATASET_CONFIG,
197
  "source_split": HF_DATASET_SPLIT,
198
  }
199
 
200
- dataset.add_sample(
201
  Sample(
202
  id=sample_id,
203
  filepath=str(image_path),
204
- label=label,
205
  metadata=metadata,
206
  )
207
  )
208
- added += 1
209
 
210
- print(f"Ingested {added} HF samples into HyperView dataset '{DATASET_NAME}'.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
 
213
  def ensure_embedding_spaces(dataset: hv.Dataset, asset_manifest: dict[str, Any], asset_dir: Path) -> None:
@@ -280,46 +450,200 @@ def ensure_layouts(dataset: hv.Dataset, asset_manifest: dict[str, Any]) -> list[
280
  return layout_keys
281
 
282
 
283
- def build_dataset() -> hv.Dataset:
284
  asset_manifest = load_asset_manifest(ASSET_MANIFEST_PATH)
 
 
 
 
 
285
 
286
- dataset = hv.Dataset(DATASET_NAME)
287
  if len(dataset) == 0:
288
  print(
289
  f"Loading HF dataset rows from {HF_DATASET_REPO}[{HF_DATASET_CONFIG}] split={HF_DATASET_SPLIT}"
290
  )
291
- ingest_hf_dataset_samples(dataset)
 
 
 
 
 
 
 
 
 
292
 
 
 
 
 
 
 
 
 
 
293
  ensure_embedding_spaces(dataset, asset_manifest=asset_manifest, asset_dir=EMBEDDING_ASSET_DIR)
 
 
 
294
  layout_keys = ensure_layouts(dataset, asset_manifest=asset_manifest)
295
 
 
 
 
 
 
 
 
 
 
 
296
  print(f"Dataset '{DATASET_NAME}' has {len(dataset)} samples")
297
  print(f"Spaces: {[space.space_key for space in dataset.list_spaces()]}")
298
  print(f"Layouts: {layout_keys}")
299
 
300
- return dataset
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
 
303
  def main() -> None:
304
  _patch_hyperview_default_panel()
305
- dataset = build_dataset()
306
 
307
- if os.environ.get("HYPERVIEW_DEMO_PREP_ONLY") == "1":
308
- print("Preparation-only mode enabled; skipping server launch.")
309
- return
 
 
 
 
 
 
 
 
 
 
 
310
 
311
  bind_host, bind_warning = _resolve_bind_host()
312
  bind_port = _resolve_port()
313
 
314
  if bind_warning:
315
  print(f"Bind host notice: {bind_warning}")
 
316
  print(
317
- "Starting HyperView with "
318
- f"bind_host={bind_host} bind_port={bind_port} "
 
319
  f"(SPACE_HOST={SPACE_HOST!r}, SPACE_PORT={os.environ.get('SPACE_PORT')!r}, "
320
  f"PORT={os.environ.get('PORT')!r})"
321
  )
322
- hv.launch(dataset, host=bind_host, port=bind_port, open_browser=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
 
325
  if __name__ == "__main__":
 
6
  import json
7
  import os
8
  import re
9
+ import threading
10
+ import time
11
+ import traceback
12
+ from datetime import datetime, timezone
13
  from pathlib import Path
14
  from typing import Any
15
 
 
18
  from datasets import DatasetDict as HFDatasetDict
19
  from datasets import load_dataset, load_from_disk
20
  import hyperview as hv
21
+ from hyperview.api import Session
22
  from hyperview.core.sample import Sample
23
 
24
  SPACE_HOST = os.environ.get("SPACE_HOST", "0.0.0.0")
 
39
  os.environ.get("EMBEDDING_ASSET_MANIFEST", str((EMBEDDING_ASSET_DIR / "manifest.json").resolve()))
40
  )
41
 
42
+ DEFAULT_STARTUP_MODE = "serve_fast"
43
+ DEFAULT_FAILURE_POLICY = "exit"
44
+ DEFAULT_BATCH_INSERT_SIZE = 500
45
+ DEFAULT_WARMUP_STATUS_PATH = Path("/tmp/hyperview_warmup_status.json")
46
+
47
+
48
+ def _utc_now() -> str:
49
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
50
+
51
+
52
+ def _resolve_startup_mode() -> str:
53
+ startup_mode = os.environ.get("HYPERVIEW_STARTUP_MODE", DEFAULT_STARTUP_MODE).strip().lower()
54
+ if startup_mode in {"serve_fast", "blocking"}:
55
+ return startup_mode
56
+ print(
57
+ f"Invalid HYPERVIEW_STARTUP_MODE={startup_mode!r}; "
58
+ f"falling back to {DEFAULT_STARTUP_MODE!r}."
59
+ )
60
+ return DEFAULT_STARTUP_MODE
61
+
62
+
63
+ def _resolve_failure_policy() -> str:
64
+ failure_policy = os.environ.get("HYPERVIEW_WARMUP_FAILURE_POLICY", DEFAULT_FAILURE_POLICY).strip().lower()
65
+ if failure_policy in {"exit", "warn"}:
66
+ return failure_policy
67
+ print(
68
+ f"Invalid HYPERVIEW_WARMUP_FAILURE_POLICY={failure_policy!r}; "
69
+ f"falling back to {DEFAULT_FAILURE_POLICY!r}."
70
+ )
71
+ return DEFAULT_FAILURE_POLICY
72
+
73
+
74
+ def _resolve_batch_insert_size() -> int:
75
+ raw_value = os.environ.get("HYPERVIEW_BATCH_INSERT_SIZE", str(DEFAULT_BATCH_INSERT_SIZE)).strip()
76
+ try:
77
+ batch_size = int(raw_value)
78
+ except ValueError as exc:
79
+ raise ValueError(f"Invalid integer value for HYPERVIEW_BATCH_INSERT_SIZE: {raw_value}") from exc
80
+ if batch_size <= 0:
81
+ raise ValueError(f"HYPERVIEW_BATCH_INSERT_SIZE must be > 0; got {batch_size}")
82
+ return batch_size
83
+
84
+
85
+ def _resolve_warmup_status_path() -> Path:
86
+ raw = os.environ.get("HYPERVIEW_WARMUP_STATUS_PATH")
87
+ if raw is None:
88
+ return DEFAULT_WARMUP_STATUS_PATH
89
+ return Path(raw)
90
+
91
+
92
+ class WarmupStatusTracker:
93
+ """Tracks warmup state and persists it to a JSON status file."""
94
+
95
+ def __init__(self, status_path: Path):
96
+ self._status_path = status_path
97
+ self._lock = threading.Lock()
98
+ now = _utc_now()
99
+ self._state: dict[str, Any] = {
100
+ "status": "starting",
101
+ "phase": "boot",
102
+ "counts": {},
103
+ "error": None,
104
+ "timestamps": {
105
+ "started_at": now,
106
+ "updated_at": now,
107
+ },
108
+ }
109
+ self._persist_locked()
110
+
111
+ def update(
112
+ self,
113
+ *,
114
+ status: str | None = None,
115
+ phase: str | None = None,
116
+ counts: dict[str, Any] | None = None,
117
+ error: dict[str, Any] | None = None,
118
+ ) -> None:
119
+ with self._lock:
120
+ now = _utc_now()
121
+ if status is not None:
122
+ self._state["status"] = status
123
+ if phase is not None:
124
+ self._state["phase"] = phase
125
+ if counts:
126
+ self._state["counts"].update(counts)
127
+ if error is not None:
128
+ self._state["error"] = error
129
+ self._state["timestamps"]["updated_at"] = now
130
+ if status == "ready":
131
+ self._state["timestamps"]["ready_at"] = now
132
+ if status == "failed":
133
+ self._state["timestamps"]["failed_at"] = now
134
+ self._persist_locked()
135
+
136
+ @property
137
+ def path(self) -> Path:
138
+ return self._status_path
139
+
140
+ def _persist_locked(self) -> None:
141
+ try:
142
+ self._status_path.parent.mkdir(parents=True, exist_ok=True)
143
+ tmp_path = self._status_path.with_name(f"{self._status_path.name}.tmp")
144
+ tmp_path.write_text(json.dumps(self._state, indent=2, sort_keys=True), encoding="utf-8")
145
+ tmp_path.replace(self._status_path)
146
+ except OSError as exc:
147
+ print(f"Warmup status warning: failed writing status JSON to {self._status_path} ({exc})")
148
+
149
 
150
  def _patch_hyperview_default_panel() -> None:
151
  """Patch HyperView 0.3.1 frontend for default panel and dock cache-key migration.
 
281
  return load_dataset(HF_DATASET_REPO, name=HF_DATASET_CONFIG, split=HF_DATASET_SPLIT)
282
 
283
 
284
+ def ingest_hf_dataset_samples(dataset: hv.Dataset, batch_insert_size: int | None = None) -> dict[str, int]:
285
  rows = _load_hf_rows()
286
  media_root = Path(os.environ.get("HYPERVIEW_MEDIA_DIR", "./demo_data/media")) / DATASET_NAME
287
  media_root.mkdir(parents=True, exist_ok=True)
288
 
289
+ effective_batch_size = _resolve_batch_insert_size() if batch_insert_size is None else int(batch_insert_size)
290
+ if effective_batch_size <= 0:
291
+ raise ValueError(f"batch_insert_size must be > 0; got {effective_batch_size}")
292
+
293
+ records_by_id: dict[str, dict[str, Any]] = {}
294
+ duplicate_ids = 0
295
  for index, row in enumerate(rows):
296
  filename = str(row.get("filename", f"sample_{index:06d}.jpg"))
297
  sample_id = str(row.get("sample_id", filename))
298
+ if sample_id in records_by_id:
299
+ duplicate_ids += 1
300
  continue
301
+ records_by_id[sample_id] = {
302
+ "index": index,
303
+ "filename": filename,
304
+ "sample_id": sample_id,
305
+ "label": str(row.get("label", "")),
306
+ "split_tag": str(row.get("split_tag", "unknown")),
307
+ }
308
 
309
+ candidate_records = list(records_by_id.values())
310
+ candidate_ids = [record["sample_id"] for record in candidate_records]
311
+ existing_ids = dataset._storage.get_existing_ids(candidate_ids) if candidate_ids else set()
312
+ missing_records = [record for record in candidate_records if record["sample_id"] not in existing_ids]
313
+
314
+ print(
315
+ "HF ingestion plan: "
316
+ f"candidates={len(candidate_records)} existing={len(existing_ids)} "
317
+ f"missing={len(missing_records)} duplicates={duplicate_ids} "
318
+ f"batch_insert_size={effective_batch_size}"
319
+ )
320
+
321
+ added = 0
322
+ saved_images = 0
323
+ pending_samples: list[Sample] = []
324
+
325
+ def flush_pending_samples() -> None:
326
+ nonlocal added
327
+ if not pending_samples:
328
+ return
329
+ dataset._storage.add_samples_batch(pending_samples)
330
+ added += len(pending_samples)
331
+ print(f"Inserted sample batch: size={len(pending_samples)} total_inserted={added}")
332
+ pending_samples.clear()
333
+
334
+ for record in missing_records:
335
+ sample_id = str(record["sample_id"])
336
+ filename = str(record["filename"])
337
+
338
+ row = rows[int(record["index"])]
339
  image_obj = row["image"]
340
  image_path = media_root / f"{Path(sample_id).stem}.jpg"
341
  if not image_path.exists():
342
  image_obj.convert("RGB").save(image_path, format="JPEG", quality=90, optimize=True)
343
+ saved_images += 1
344
 
 
345
  metadata = {
346
  "filename": filename,
347
  "sample_id": sample_id,
348
+ "split_tag": str(record["split_tag"]),
349
+ "identity": str(record["label"]),
350
  "source_repo": HF_DATASET_REPO,
351
  "source_config": HF_DATASET_CONFIG,
352
  "source_split": HF_DATASET_SPLIT,
353
  }
354
 
355
+ pending_samples.append(
356
  Sample(
357
  id=sample_id,
358
  filepath=str(image_path),
359
+ label=str(record["label"]),
360
  metadata=metadata,
361
  )
362
  )
 
363
 
364
+ if len(pending_samples) >= effective_batch_size:
365
+ flush_pending_samples()
366
+
367
+ flush_pending_samples()
368
+
369
+ print(
370
+ f"Ingested {added} HF samples into HyperView dataset '{DATASET_NAME}' "
371
+ f"(saved_images={saved_images}, existing={len(existing_ids)})."
372
+ )
373
+
374
+ return {
375
+ "candidates": len(candidate_records),
376
+ "existing": len(existing_ids),
377
+ "added": added,
378
+ "saved_images": saved_images,
379
+ "duplicates": duplicate_ids,
380
+ }
381
 
382
 
383
  def ensure_embedding_spaces(dataset: hv.Dataset, asset_manifest: dict[str, Any], asset_dir: Path) -> None:
 
450
  return layout_keys
451
 
452
 
453
+ def _run_warmup(dataset: hv.Dataset, tracker: WarmupStatusTracker) -> None:
454
  asset_manifest = load_asset_manifest(ASSET_MANIFEST_PATH)
455
+ tracker.update(
456
+ status="running",
457
+ phase="ingest",
458
+ counts={"manifest_models": len(asset_manifest.get("models", []))},
459
+ )
460
 
461
+ batch_insert_size = _resolve_batch_insert_size()
462
  if len(dataset) == 0:
463
  print(
464
  f"Loading HF dataset rows from {HF_DATASET_REPO}[{HF_DATASET_CONFIG}] split={HF_DATASET_SPLIT}"
465
  )
466
+ ingest_stats = ingest_hf_dataset_samples(dataset, batch_insert_size=batch_insert_size)
467
+ else:
468
+ ingest_stats = {
469
+ "candidates": len(dataset),
470
+ "existing": len(dataset),
471
+ "added": 0,
472
+ "saved_images": 0,
473
+ "duplicates": 0,
474
+ }
475
+ print(f"Skipping HF ingestion because dataset '{DATASET_NAME}' already has {len(dataset)} samples.")
476
 
477
+ tracker.update(
478
+ counts={
479
+ "batch_insert_size": batch_insert_size,
480
+ "dataset_samples": len(dataset),
481
+ **ingest_stats,
482
+ }
483
+ )
484
+
485
+ tracker.update(phase="spaces")
486
  ensure_embedding_spaces(dataset, asset_manifest=asset_manifest, asset_dir=EMBEDDING_ASSET_DIR)
487
+ tracker.update(counts={"spaces": len(dataset.list_spaces())})
488
+
489
+ tracker.update(phase="layouts")
490
  layout_keys = ensure_layouts(dataset, asset_manifest=asset_manifest)
491
 
492
+ tracker.update(
493
+ status="ready",
494
+ phase="ready",
495
+ counts={
496
+ "dataset_samples": len(dataset),
497
+ "spaces": len(dataset.list_spaces()),
498
+ "layouts": len(layout_keys),
499
+ },
500
+ )
501
+
502
  print(f"Dataset '{DATASET_NAME}' has {len(dataset)} samples")
503
  print(f"Spaces: {[space.space_key for space in dataset.list_spaces()]}")
504
  print(f"Layouts: {layout_keys}")
505
 
506
+
507
+ def _run_warmup_blocking(dataset: hv.Dataset, tracker: WarmupStatusTracker) -> None:
508
+ try:
509
+ _run_warmup(dataset, tracker)
510
+ except Exception as exc:
511
+ tb = traceback.format_exc()
512
+ tracker.update(
513
+ status="failed",
514
+ phase="failed",
515
+ error={
516
+ "type": type(exc).__name__,
517
+ "message": str(exc),
518
+ "traceback": tb,
519
+ },
520
+ )
521
+ print(tb)
522
+ raise
523
+
524
+
525
+ def _warmup_worker(
526
+ dataset: hv.Dataset,
527
+ tracker: WarmupStatusTracker,
528
+ failure_policy: str,
529
+ failure_event: threading.Event,
530
+ failure_holder: dict[str, str],
531
+ ) -> None:
532
+ try:
533
+ _run_warmup(dataset, tracker)
534
+ except Exception as exc:
535
+ tb = traceback.format_exc()
536
+ tracker.update(
537
+ status="failed",
538
+ phase="failed",
539
+ error={
540
+ "type": type(exc).__name__,
541
+ "message": str(exc),
542
+ "traceback": tb,
543
+ },
544
+ )
545
+ print("Warmup failed:")
546
+ print(tb)
547
+ failure_holder["error"] = f"{type(exc).__name__}: {exc}"
548
+ if failure_policy == "exit":
549
+ failure_event.set()
550
+
551
+
552
+ def _start_server_session(dataset: hv.Dataset, bind_host: str, bind_port: int) -> Session:
553
+ session = Session(dataset, host=bind_host, port=bind_port)
554
+ session.start(background=True)
555
+ print(f"HyperView server is running at {session.url}")
556
+ return session
557
+
558
+
559
+ def _serve_forever(
560
+ session: Session,
561
+ *,
562
+ failure_event: threading.Event | None = None,
563
+ failure_holder: dict[str, str] | None = None,
564
+ ) -> None:
565
+ try:
566
+ while True:
567
+ time.sleep(0.25)
568
+ if session._server_thread is not None and not session._server_thread.is_alive():
569
+ raise RuntimeError("HyperView server stopped unexpectedly.")
570
+
571
+ if failure_event is not None and failure_event.is_set():
572
+ reason = None
573
+ if failure_holder is not None:
574
+ reason = failure_holder.get("error")
575
+ if reason:
576
+ raise RuntimeError(f"Warmup failed and failure policy is 'exit': {reason}")
577
+ raise RuntimeError("Warmup failed and failure policy is 'exit'.")
578
+ except KeyboardInterrupt:
579
+ pass
580
+ finally:
581
+ session.stop()
582
+ if session._server_thread is not None:
583
+ session._server_thread.join(timeout=2.0)
584
 
585
 
586
  def main() -> None:
587
  _patch_hyperview_default_panel()
 
588
 
589
+ startup_mode = _resolve_startup_mode()
590
+ failure_policy = _resolve_failure_policy()
591
+ warmup_status_path = _resolve_warmup_status_path()
592
+
593
+ dataset = hv.Dataset(DATASET_NAME)
594
+ tracker = WarmupStatusTracker(warmup_status_path)
595
+ tracker.update(
596
+ counts={
597
+ "dataset_samples": len(dataset),
598
+ "startup_mode": startup_mode,
599
+ "failure_policy": failure_policy,
600
+ "batch_insert_size": _resolve_batch_insert_size(),
601
+ }
602
+ )
603
 
604
  bind_host, bind_warning = _resolve_bind_host()
605
  bind_port = _resolve_port()
606
 
607
  if bind_warning:
608
  print(f"Bind host notice: {bind_warning}")
609
+
610
  print(
611
+ "Starting HyperView runtime with "
612
+ f"startup_mode={startup_mode} failure_policy={failure_policy} "
613
+ f"status_path={warmup_status_path} bind_host={bind_host} bind_port={bind_port} "
614
  f"(SPACE_HOST={SPACE_HOST!r}, SPACE_PORT={os.environ.get('SPACE_PORT')!r}, "
615
  f"PORT={os.environ.get('PORT')!r})"
616
  )
617
+
618
+ if os.environ.get("HYPERVIEW_DEMO_PREP_ONLY") == "1":
619
+ _run_warmup_blocking(dataset, tracker)
620
+ print("Preparation-only mode enabled; skipping server launch.")
621
+ return
622
+
623
+ if startup_mode == "blocking":
624
+ _run_warmup_blocking(dataset, tracker)
625
+ session = _start_server_session(dataset, bind_host=bind_host, bind_port=bind_port)
626
+ _serve_forever(session)
627
+ return
628
+
629
+ failure_event = threading.Event()
630
+ failure_holder: dict[str, str] = {}
631
+
632
+ warmup_thread = threading.Thread(
633
+ target=_warmup_worker,
634
+ name="hyperview-warmup",
635
+ args=(dataset, tracker, failure_policy, failure_event, failure_holder),
636
+ daemon=True,
637
+ )
638
+ warmup_thread.start()
639
+ print("Warmup thread started in background.")
640
+
641
+ session = _start_server_session(dataset, bind_host=bind_host, bind_port=bind_port)
642
+
643
+ if failure_policy == "exit":
644
+ _serve_forever(session, failure_event=failure_event, failure_holder=failure_holder)
645
+ else:
646
+ _serve_forever(session)
647
 
648
 
649
  if __name__ == "__main__":