raylim Claude Opus 4.6 commited on
Commit
99f61a7
·
1 Parent(s): e297f75

fix: use cached_slide_count instead of from_cache boolean for accurate telemetry

Browse files

Replace boolean from_cache flag with integer cached_slide_count to properly
track cache hits at the slide level instead of batch level. This fixes
incorrect average duration calculation for mixed batches (some cached,
some fresh slides).

Previous behavior:
- Mixed batch (3 cached, 7 fresh) with 70s duration was excluded from
average if any slide was cached

New behavior:
- Only fully cached batches (all slides cached) are excluded from average
- Mixed batches are correctly included in average duration
- Reports now show total cached slides and cache hit rate percentage

Changes:
- UsageEvent: from_cache (bool) -> cached_slide_count (int)
- TelemetryTracker: Updated session metrics to check if all slides cached
- UI: Track cached_slides and fresh_slides counters separately
- Reports: Show 'Cached slides: X' and 'Cache hit rate: Y%'
- Tests: Updated all tests to use new cached_slide_count field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

scripts/telemetry_report.py CHANGED
@@ -205,10 +205,19 @@ def generate_text_report(telemetry_dir: Path, date: Optional[str] = None) -> str
205
  )
206
 
207
  # Cache hit tracking
208
- cached_analyses = [c for c in completes if c.get("from_cache")]
209
- fresh_analyses = [c for c in completes if not c.get("from_cache")]
 
 
 
 
 
 
 
 
 
210
 
211
- # Calculate average duration (excluding cached analyses)
212
  durations = [
213
  c.get("duration_sec", 0) for c in fresh_analyses if c.get("duration_sec")
214
  ]
@@ -219,8 +228,10 @@ def generate_text_report(telemetry_dir: Path, date: Optional[str] = None) -> str
219
  lines.append(f"Analyses completed: {len(completes)}")
220
  lines.append(f"Successful analyses: {len(successful)}")
221
  lines.append(f"Total slides processed: {total_slides}")
222
- if cached_analyses:
223
- lines.append(f"Cached analyses: {len(cached_analyses)}")
 
 
224
  lines.append(f"Unique sessions: {unique_sessions}")
225
  if avg_duration > 0:
226
  lines.append(f"Average analysis duration: {avg_duration:.1f}s")
@@ -438,7 +449,9 @@ def generate_html_report(telemetry_dir: Path, date: Optional[str] = None) -> str
438
  )
439
 
440
  # Cache hit tracking
441
- cached_analyses = [c for c in completes if c.get("from_cache")]
 
 
442
 
443
  html.append("<h2>Usage Summary</h2>")
444
  html.append("<table>")
@@ -448,9 +461,13 @@ def generate_html_report(telemetry_dir: Path, date: Optional[str] = None) -> str
448
  f"<tr><td>Successful analyses</td><td class='success'>{len(successful)}</td></tr>"
449
  )
450
  html.append(f"<tr><td>Total slides</td><td>{total_slides}</td></tr>")
451
- if cached_analyses:
 
 
 
 
452
  html.append(
453
- f"<tr><td>Cached analyses</td><td>{len(cached_analyses)}</td></tr>"
454
  )
455
  html.append(f"<tr><td>Unique sessions</td><td>{unique_sessions}</td></tr>")
456
  html.append("</table>")
 
205
  )
206
 
207
  # Cache hit tracking
208
+ total_cached_slides = sum(
209
+ c.get("cached_slide_count", 0) for c in completes if c.get("cached_slide_count")
210
+ )
211
+ fully_cached_analyses = [
212
+ c for c in completes
213
+ if c.get("cached_slide_count") and c.get("cached_slide_count") == c.get("slide_count", 0)
214
+ ]
215
+ fresh_analyses = [
216
+ c for c in completes
217
+ if not c.get("cached_slide_count") or c.get("cached_slide_count") < c.get("slide_count", 0)
218
+ ]
219
 
220
+ # Calculate average duration (excluding fully cached analyses)
221
  durations = [
222
  c.get("duration_sec", 0) for c in fresh_analyses if c.get("duration_sec")
223
  ]
 
228
  lines.append(f"Analyses completed: {len(completes)}")
229
  lines.append(f"Successful analyses: {len(successful)}")
230
  lines.append(f"Total slides processed: {total_slides}")
231
+ if total_cached_slides > 0:
232
+ lines.append(f"Cached slides: {total_cached_slides}")
233
+ cache_rate = (total_cached_slides / total_slides * 100) if total_slides > 0 else 0
234
+ lines.append(f"Cache hit rate: {cache_rate:.1f}%")
235
  lines.append(f"Unique sessions: {unique_sessions}")
236
  if avg_duration > 0:
237
  lines.append(f"Average analysis duration: {avg_duration:.1f}s")
 
449
  )
450
 
451
  # Cache hit tracking
452
+ total_cached_slides = sum(
453
+ c.get("cached_slide_count", 0) for c in completes if c.get("cached_slide_count")
454
+ )
455
 
456
  html.append("<h2>Usage Summary</h2>")
457
  html.append("<table>")
 
461
  f"<tr><td>Successful analyses</td><td class='success'>{len(successful)}</td></tr>"
462
  )
463
  html.append(f"<tr><td>Total slides</td><td>{total_slides}</td></tr>")
464
+ if total_cached_slides > 0:
465
+ html.append(
466
+ f"<tr><td>Cached slides</td><td>{total_cached_slides}</td></tr>"
467
+ )
468
+ cache_rate = (total_cached_slides / total_slides * 100) if total_slides > 0 else 0
469
  html.append(
470
+ f"<tr><td>Cache hit rate</td><td>{cache_rate:.1f}%</td></tr>"
471
  )
472
  html.append(f"<tr><td>Unique sessions</td><td>{unique_sessions}</td></tr>")
473
  html.append("</table>")
src/mosaic/telemetry/events.py CHANGED
@@ -65,7 +65,9 @@ class UsageEvent:
65
  gpu_type: Optional[str] = None # From hardware.GPU_TYPE
66
  duration_sec: Optional[float] = None # On completion only
67
  success: Optional[bool] = None # On completion only
68
- from_cache: Optional[bool] = None # On completion only
 
 
69
  is_logged_in: Optional[bool] = None # True if HF user logged in
70
  hf_username: Optional[str] = None # Raw HF username (HF Spaces only)
71
  event_id: str = field(default_factory=_generate_event_id)
 
65
  gpu_type: Optional[str] = None # From hardware.GPU_TYPE
66
  duration_sec: Optional[float] = None # On completion only
67
  success: Optional[bool] = None # On completion only
68
+ cached_slide_count: Optional[int] = (
69
+ None # Number of cached slides (completion only)
70
+ )
71
  is_logged_in: Optional[bool] = None # True if HF user logged in
72
  hf_username: Optional[str] = None # Raw HF username (HF Spaces only)
73
  event_id: str = field(default_factory=_generate_event_id)
src/mosaic/telemetry/tracker.py CHANGED
@@ -215,7 +215,7 @@ class TelemetryTracker:
215
  gpu_type: Optional[str] = None,
216
  duration_sec: Optional[float] = None,
217
  success: Optional[bool] = None,
218
- from_cache: Optional[bool] = None,
219
  is_logged_in: Optional[bool] = None,
220
  hf_username: Optional[str] = None,
221
  ) -> None:
@@ -232,7 +232,7 @@ class TelemetryTracker:
232
  gpu_type: GPU type string
233
  duration_sec: Analysis duration (for analysis_complete only)
234
  success: Whether analysis succeeded (for analysis_complete only)
235
- from_cache: True if result was served from cache (for analysis_complete only)
236
  is_logged_in: True if HF user logged in
237
  hf_username: HF username (HF Spaces only)
238
  """
@@ -250,7 +250,7 @@ class TelemetryTracker:
250
  gpu_type=gpu_type,
251
  duration_sec=duration_sec,
252
  success=success,
253
- from_cache=from_cache,
254
  is_logged_in=is_logged_in,
255
  hf_username=hf_username,
256
  )
@@ -260,8 +260,13 @@ class TelemetryTracker:
260
  if event_type == "analysis_complete" and duration_sec is not None:
261
  with self._session_lock:
262
  self._analysis_count += 1
263
- # Don't count duration if result was served from cache
264
- if not from_cache:
 
 
 
 
 
265
  self._analysis_time_sec += duration_sec
266
 
267
  # =========================================================================
 
215
  gpu_type: Optional[str] = None,
216
  duration_sec: Optional[float] = None,
217
  success: Optional[bool] = None,
218
+ cached_slide_count: Optional[int] = None,
219
  is_logged_in: Optional[bool] = None,
220
  hf_username: Optional[str] = None,
221
  ) -> None:
 
232
  gpu_type: GPU type string
233
  duration_sec: Analysis duration (for analysis_complete only)
234
  success: Whether analysis succeeded (for analysis_complete only)
235
+ cached_slide_count: Number of slides served from cache (for analysis_complete only)
236
  is_logged_in: True if HF user logged in
237
  hf_username: HF username (HF Spaces only)
238
  """
 
250
  gpu_type=gpu_type,
251
  duration_sec=duration_sec,
252
  success=success,
253
+ cached_slide_count=cached_slide_count,
254
  is_logged_in=is_logged_in,
255
  hf_username=hf_username,
256
  )
 
260
  if event_type == "analysis_complete" and duration_sec is not None:
261
  with self._session_lock:
262
  self._analysis_count += 1
263
+ # Don't count duration if ALL slides were served from cache
264
+ is_fully_cached = (
265
+ cached_slide_count is not None
266
+ and slide_count > 0
267
+ and cached_slide_count == slide_count
268
+ )
269
+ if not is_fully_cached:
270
  self._analysis_time_sec += duration_sec
271
 
272
  # =========================================================================
src/mosaic/ui/app.py CHANGED
@@ -289,7 +289,8 @@ def analyze_slides(
289
  all_slide_masks = []
290
  all_aeon_results = []
291
  all_paladin_results = []
292
- from_cache = False
 
293
 
294
  # Log analysis start event
295
  tracker.log_usage_event(
@@ -402,7 +403,7 @@ def analyze_slides(
402
  cached = load_analysis_results(file_uuid, settings_hash)
403
  if cached is not None:
404
  slide_mask, aeon_results, paladin_results = cached
405
- from_cache = True
406
  logger.info(
407
  f"Using cached results for {file_uuid}/{settings_hash}"
408
  )
@@ -416,6 +417,7 @@ def analyze_slides(
416
 
417
  # If no cached results, run analysis
418
  if slide_mask is None or aeon_results is None or paladin_results is None:
 
419
  # Lazy model loading: load on first cache miss
420
  if model_cache is None:
421
  if IS_T4_GPU:
@@ -514,7 +516,7 @@ def analyze_slides(
514
  slide_count=len(slides),
515
  duration_sec=duration_sec,
516
  success=True,
517
- from_cache=from_cache if from_cache else None,
518
  gpu_type=GPU_TYPE,
519
  is_logged_in=user_info.is_logged_in,
520
  hf_username=user_info.username,
 
289
  all_slide_masks = []
290
  all_aeon_results = []
291
  all_paladin_results = []
292
+ cached_slides = 0
293
+ fresh_slides = 0
294
 
295
  # Log analysis start event
296
  tracker.log_usage_event(
 
403
  cached = load_analysis_results(file_uuid, settings_hash)
404
  if cached is not None:
405
  slide_mask, aeon_results, paladin_results = cached
406
+ cached_slides += 1
407
  logger.info(
408
  f"Using cached results for {file_uuid}/{settings_hash}"
409
  )
 
417
 
418
  # If no cached results, run analysis
419
  if slide_mask is None or aeon_results is None or paladin_results is None:
420
+ fresh_slides += 1
421
  # Lazy model loading: load on first cache miss
422
  if model_cache is None:
423
  if IS_T4_GPU:
 
516
  slide_count=len(slides),
517
  duration_sec=duration_sec,
518
  success=True,
519
+ cached_slide_count=cached_slides if cached_slides > 0 else None,
520
  gpu_type=GPU_TYPE,
521
  is_logged_in=user_info.is_logged_in,
522
  hf_username=user_info.username,
tests/telemetry/test_events.py CHANGED
@@ -108,24 +108,24 @@ class TestUsageEvent:
108
  assert data["slide_count"] == 3
109
  assert data["session_hash"] is None
110
 
111
- def test_from_cache_true(self):
112
- """Test from_cache field on analysis_complete event."""
113
  event = UsageEvent(
114
  event_type="analysis_complete",
115
  analysis_id="test-123",
116
  session_hash=None,
117
- slide_count=1,
118
  duration_sec=2.0,
119
  success=True,
120
- from_cache=True,
121
  )
122
 
123
- assert event.from_cache is True
124
  data = event.to_dict()
125
- assert data["from_cache"] is True
126
 
127
- def test_from_cache_default_none(self):
128
- """Test that from_cache defaults to None for backward compat."""
129
  event = UsageEvent(
130
  event_type="analysis_complete",
131
  analysis_id="test-123",
@@ -135,9 +135,9 @@ class TestUsageEvent:
135
  success=True,
136
  )
137
 
138
- assert event.from_cache is None
139
  data = event.to_dict()
140
- assert data["from_cache"] is None
141
 
142
  def test_user_info_fields(self):
143
  """Test is_logged_in and hf_username fields in UsageEvent."""
 
108
  assert data["slide_count"] == 3
109
  assert data["session_hash"] is None
110
 
111
+ def test_cached_slide_count(self):
112
+ """Test cached_slide_count field on analysis_complete event."""
113
  event = UsageEvent(
114
  event_type="analysis_complete",
115
  analysis_id="test-123",
116
  session_hash=None,
117
+ slide_count=10,
118
  duration_sec=2.0,
119
  success=True,
120
+ cached_slide_count=3,
121
  )
122
 
123
+ assert event.cached_slide_count == 3
124
  data = event.to_dict()
125
+ assert data["cached_slide_count"] == 3
126
 
127
+ def test_cached_slide_count_default_none(self):
128
+ """Test that cached_slide_count defaults to None."""
129
  event = UsageEvent(
130
  event_type="analysis_complete",
131
  analysis_id="test-123",
 
135
  success=True,
136
  )
137
 
138
+ assert event.cached_slide_count is None
139
  data = event.to_dict()
140
+ assert data["cached_slide_count"] is None
141
 
142
  def test_user_info_fields(self):
143
  """Test is_logged_in and hf_username fields in UsageEvent."""
tests/telemetry/test_report.py CHANGED
@@ -288,11 +288,12 @@ class TestGenerateTextReport:
288
  assert "for 2026-01-20" in report
289
 
290
  def test_report_with_cached_analysis(self, tmp_path):
291
- """Test report shows cached analysis count when present."""
292
  daily_dir = tmp_path / "daily"
293
  daily_dir.mkdir()
294
 
295
  usage = [
 
296
  {
297
  "event_type": "analysis_start",
298
  "slide_count": 1,
@@ -305,8 +306,9 @@ class TestGenerateTextReport:
305
  "success": True,
306
  "duration_sec": 2.0,
307
  "slide_count": 1,
308
- "from_cache": True,
309
  },
 
310
  {
311
  "event_type": "analysis_start",
312
  "slide_count": 1,
@@ -319,6 +321,7 @@ class TestGenerateTextReport:
319
  "success": True,
320
  "duration_sec": 120.0,
321
  "slide_count": 1,
 
322
  },
323
  ]
324
 
@@ -328,8 +331,10 @@ class TestGenerateTextReport:
328
  f.write(json.dumps(event) + "\n")
329
 
330
  report = generate_text_report(tmp_path)
331
- assert "Cached analyses: 1" in report
332
- # Average duration should exclude cached analysis (2s),
 
 
333
  # only include the fresh one (120s)
334
  assert "Average analysis duration: 120.0s" in report
335
 
@@ -360,7 +365,8 @@ class TestGenerateTextReport:
360
  f.write(json.dumps(event) + "\n")
361
 
362
  report = generate_text_report(tmp_path)
363
- assert "Cached analyses" not in report
 
364
 
365
  def test_report_with_resources(self, tmp_path):
366
  """Test report with resource data."""
@@ -423,7 +429,7 @@ class TestGenerateHtmlReport:
423
  assert "<td>1</td>" in report # 1 analysis started
424
 
425
  def test_html_with_cached_analysis(self, tmp_path):
426
- """Test HTML report shows cached analysis count."""
427
  daily_dir = tmp_path / "daily"
428
  daily_dir.mkdir()
429
 
@@ -438,7 +444,7 @@ class TestGenerateHtmlReport:
438
  "success": True,
439
  "duration_sec": 2.0,
440
  "slide_count": 1,
441
- "from_cache": True,
442
  },
443
  ]
444
 
@@ -448,7 +454,8 @@ class TestGenerateHtmlReport:
448
  f.write(json.dumps(event) + "\n")
449
 
450
  report = generate_html_report(tmp_path)
451
- assert "Cached analyses" in report
 
452
 
453
  def test_html_with_failures(self, tmp_path):
454
  """Test HTML report with failures."""
 
288
  assert "for 2026-01-20" in report
289
 
290
  def test_report_with_cached_analysis(self, tmp_path):
291
+ """Test report shows cached slide count and cache hit rate."""
292
  daily_dir = tmp_path / "daily"
293
  daily_dir.mkdir()
294
 
295
  usage = [
296
+ # Batch 1: Fully cached (1/1 slides cached)
297
  {
298
  "event_type": "analysis_start",
299
  "slide_count": 1,
 
306
  "success": True,
307
  "duration_sec": 2.0,
308
  "slide_count": 1,
309
+ "cached_slide_count": 1, # Fully cached
310
  },
311
+ # Batch 2: Fresh (0/1 slides cached)
312
  {
313
  "event_type": "analysis_start",
314
  "slide_count": 1,
 
321
  "success": True,
322
  "duration_sec": 120.0,
323
  "slide_count": 1,
324
+ # No cached_slide_count = 0 cached
325
  },
326
  ]
327
 
 
331
  f.write(json.dumps(event) + "\n")
332
 
333
  report = generate_text_report(tmp_path)
334
+ # Should show 1 cached slide out of 2 total
335
+ assert "Cached slides: 1" in report
336
+ assert "Cache hit rate: 50.0%" in report
337
+ # Average duration should exclude fully cached batch (2s),
338
  # only include the fresh one (120s)
339
  assert "Average analysis duration: 120.0s" in report
340
 
 
365
  f.write(json.dumps(event) + "\n")
366
 
367
  report = generate_text_report(tmp_path)
368
+ assert "Cached slides" not in report
369
+ assert "Cache hit rate" not in report
370
 
371
  def test_report_with_resources(self, tmp_path):
372
  """Test report with resource data."""
 
429
  assert "<td>1</td>" in report # 1 analysis started
430
 
431
  def test_html_with_cached_analysis(self, tmp_path):
432
+ """Test HTML report shows cached slide count and hit rate."""
433
  daily_dir = tmp_path / "daily"
434
  daily_dir.mkdir()
435
 
 
444
  "success": True,
445
  "duration_sec": 2.0,
446
  "slide_count": 1,
447
+ "cached_slide_count": 1,
448
  },
449
  ]
450
 
 
454
  f.write(json.dumps(event) + "\n")
455
 
456
  report = generate_html_report(tmp_path)
457
+ assert "Cached slides" in report
458
+ assert "Cache hit rate" in report
459
 
460
  def test_html_with_failures(self, tmp_path):
461
  """Test HTML report with failures."""
tests/telemetry/test_tracker.py CHANGED
@@ -196,49 +196,49 @@ class TestUsageEvents:
196
  assert metrics["analysis_count"] == 2
197
  assert metrics["analysis_time_sec"] == 105.0
198
 
199
- def test_log_usage_event_with_from_cache(self, tracker, temp_dir):
200
- """Test that from_cache is persisted in usage events."""
201
  tracker.log_usage_event(
202
  event_type="analysis_complete",
203
  analysis_id="test-cache",
204
- slide_count=1,
205
- duration_sec=2.0,
206
  success=True,
207
- from_cache=True,
208
  )
209
 
210
  usage_files = list((temp_dir / "daily").glob("usage_*.jsonl"))
211
  with open(usage_files[0]) as f:
212
  event = json.loads(f.read().strip())
213
 
214
- assert event["from_cache"] is True
215
 
216
- def test_cached_analysis_excludes_duration_from_metrics(self, tracker):
217
- """Test that cached analyses don't count toward analysis_time_sec."""
218
  tracker.log_app_start()
219
  tracker.log_usage_event(
220
  event_type="analysis_complete",
221
  analysis_id="test-cache",
222
- slide_count=1,
223
  duration_sec=2.0,
224
  success=True,
225
- from_cache=True,
226
  )
227
 
228
  metrics = tracker._get_session_metrics()
229
  assert metrics["analysis_count"] == 1
230
  assert metrics["analysis_time_sec"] == 0.0
231
 
232
- def test_fresh_analysis_includes_duration_in_metrics(self, tracker):
233
- """Test that fresh analyses count toward analysis_time_sec."""
234
  tracker.log_app_start()
235
  tracker.log_usage_event(
236
  event_type="analysis_complete",
237
- analysis_id="test-fresh",
238
- slide_count=1,
239
  duration_sec=60.0,
240
  success=True,
241
- from_cache=False,
242
  )
243
 
244
  metrics = tracker._get_session_metrics()
 
196
  assert metrics["analysis_count"] == 2
197
  assert metrics["analysis_time_sec"] == 105.0
198
 
199
+ def test_log_usage_event_with_cached_slides(self, tracker, temp_dir):
200
+ """Test that cached_slide_count is persisted in usage events."""
201
  tracker.log_usage_event(
202
  event_type="analysis_complete",
203
  analysis_id="test-cache",
204
+ slide_count=10,
205
+ duration_sec=50.0,
206
  success=True,
207
+ cached_slide_count=3,
208
  )
209
 
210
  usage_files = list((temp_dir / "daily").glob("usage_*.jsonl"))
211
  with open(usage_files[0]) as f:
212
  event = json.loads(f.read().strip())
213
 
214
+ assert event["cached_slide_count"] == 3
215
 
216
+ def test_fully_cached_analysis_excludes_duration_from_metrics(self, tracker):
217
+ """Test that fully cached analyses don't count toward analysis_time_sec."""
218
  tracker.log_app_start()
219
  tracker.log_usage_event(
220
  event_type="analysis_complete",
221
  analysis_id="test-cache",
222
+ slide_count=5,
223
  duration_sec=2.0,
224
  success=True,
225
+ cached_slide_count=5, # All slides cached
226
  )
227
 
228
  metrics = tracker._get_session_metrics()
229
  assert metrics["analysis_count"] == 1
230
  assert metrics["analysis_time_sec"] == 0.0
231
 
232
+ def test_mixed_cache_analysis_includes_duration_in_metrics(self, tracker):
233
+ """Test that mixed analyses (some cached) count toward analysis_time_sec."""
234
  tracker.log_app_start()
235
  tracker.log_usage_event(
236
  event_type="analysis_complete",
237
+ analysis_id="test-mixed",
238
+ slide_count=10,
239
  duration_sec=60.0,
240
  success=True,
241
+ cached_slide_count=3, # Only 3/10 cached
242
  )
243
 
244
  metrics = tracker._get_session_metrics()