from __future__ import annotations import csv import json import time from collections import Counter from itertools import islice from pathlib import Path import psutil from tqdm.auto import tqdm from .layout import DETAILS_SUMMARY_FILENAME, preferred_details_dir, resolve_details_dir PROGRESS_FLUSH_INTERVAL_S = 5.0 PROGRESS_REFRESH_INTERVAL_S = 0.5 PROGRESS_REFRESH_INTERVAL_REQ = 256 DEFAULT_INPUT_LENGTH_BUCKET_THRESHOLDS = [32 * 1024, 85 * 1024, 128 * 1024] FIRST_SEEN_MS = 0 LAST_SEEN_MS = 1 LAST_REUSE_MS = 2 FIRST_REQUEST_ID = 3 LAST_REQUEST_ID = 4 LAST_REUSE_REQUEST_ID = 5 REUSE_COUNT = 6 def _format_bucket_boundary(value: int) -> str: if value == 0: return "0" if value % (1024 * 1024) == 0: return f"{value // (1024 * 1024)}Mi" if value % 1024 == 0: return f"{value // 1024}Ki" return str(value) def build_input_length_bucket_defs(thresholds=None): parsed_thresholds = ( list(DEFAULT_INPUT_LENGTH_BUCKET_THRESHOLDS) if thresholds is None else sorted(set(int(value) for value in thresholds)) ) if not parsed_thresholds: raise ValueError("At least one input-length bucket threshold is required.") if any(value <= 0 for value in parsed_thresholds): raise ValueError("Input-length bucket thresholds must be positive integers.") if parsed_thresholds == DEFAULT_INPUT_LENGTH_BUCKET_THRESHOLDS: return [ ("0-32Ki", 0, 32 * 1024), ("32-85Ki", 32 * 1024, 85 * 1024), ("85-128Ki", 85 * 1024, 128 * 1024), ("128Ki+", 128 * 1024, None), ] bucket_defs = [] lower_bound = 0 for upper_bound in parsed_thresholds: bucket_defs.append( ( f"{_format_bucket_boundary(lower_bound)}-{_format_bucket_boundary(upper_bound)}", lower_bound, upper_bound, ) ) lower_bound = upper_bound bucket_defs.append((f"{_format_bucket_boundary(lower_bound)}+", lower_bound, None)) return bucket_defs def assign_input_length_bucket(input_tokens: int, bucket_defs=None) -> str: bucket_defs = bucket_defs or build_input_length_bucket_defs() for bucket_label, lower_bound, upper_bound in bucket_defs: if input_tokens >= lower_bound and (upper_bound is None or input_tokens < upper_bound): return bucket_label return bucket_defs[-1][0] def write_csv(path: Path, rows: list[dict], fieldnames: list[str] | None = None) -> Path: path.parent.mkdir(parents=True, exist_ok=True) if fieldnames is None and rows: fieldnames = list(rows[0].keys()) fieldnames = fieldnames or [] with path.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=fieldnames) if fieldnames: writer.writeheader() if rows: writer.writerows(rows) return path def _estimate_peak_rss_mb(current_rss_mb, peak_rss_mb, fraction_done): baseline = max(current_rss_mb, peak_rss_mb) headroom = 1.0 + 0.25 * max(0.0, 1.0 - fraction_done) return baseline * headroom def _progress_postfix(process, peak_rss_mb, fraction_done, **extra): current_rss_mb = process.memory_info().rss / (1024 * 1024) peak_rss_mb = max(peak_rss_mb, current_rss_mb) postfix = { "rss_mb": f"{current_rss_mb:.0f}", "est_peak_mb": f"{_estimate_peak_rss_mb(current_rss_mb, peak_rss_mb, fraction_done):.0f}", } postfix.update(extra) return postfix, peak_rss_mb def _format_duration(seconds): if seconds is None or seconds < 0: return "?" if seconds < 60: return f"{seconds:.0f}s" if seconds < 3600: return f"{seconds / 60:.1f}m" return f"{seconds / 3600:.2f}h" def _write_progress_state( path, *, total_requests, processed_requests, started_at, current_rss_mb, peak_rss_mb, est_peak_mb, source_path, features_path, last_request_id, block_state_count, bucket_state_count, ): elapsed_s = max(time.monotonic() - started_at, 1e-9) req_per_s = processed_requests / elapsed_s eta_s = ((total_requests - processed_requests) / req_per_s) if req_per_s > 0 and processed_requests < total_requests else 0.0 payload = { "source_path": str(source_path), "features_path": str(features_path), "total_requests": total_requests, "processed_requests": processed_requests, "fraction_done": (processed_requests / total_requests) if total_requests else 1.0, "elapsed_s": elapsed_s, "req_per_s": req_per_s, "eta_s": eta_s, "eta_human": _format_duration(eta_s), "rss_mb": current_rss_mb, "peak_rss_mb": peak_rss_mb, "est_peak_mb": est_peak_mb, "block_state_count": block_state_count, "bucket_state_count": bucket_state_count, "last_request_id": last_request_id, "updated_at_epoch_s": time.time(), } tmp_path = path.with_suffix(path.suffix + ".tmp") tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") tmp_path.replace(path) def _count_lines(path): with open(path, "r", encoding="utf-8") as handle: return sum(1 for _ in handle) def _count_feature_rows(path): total_lines = _count_lines(path) return max(total_lines - 1, 0) class InMemoryBlockCache: def __init__(self): self.state = {} def get(self, block_id): return self.state.get(block_id) def put(self, block_id, meta): self.state[block_id] = meta def iter_blocks(self): for block_id, meta in self.state.items(): yield ( block_id, meta[FIRST_SEEN_MS], meta[LAST_SEEN_MS], meta[LAST_REUSE_MS], meta[FIRST_REQUEST_ID], meta[LAST_REQUEST_ID], meta[LAST_REUSE_REQUEST_ID], meta[REUSE_COUNT], ) def __len__(self): return len(self.state) def _normalize_source_row(row): meta = row.get("meta", {}) if isinstance(row.get("meta", {}), dict) else {} declared_tools = row.get("declared_tools", []) raw_messages = row.get("raw_messages", []) return { "meta": meta, "declared_tools": [tool for tool in declared_tools if isinstance(tool, dict)], "raw_messages": [message for message in raw_messages if isinstance(message, dict)], } def _read_source_minimal(path): with open(path, "r", encoding="utf-8") as handle: for line in handle: row = _normalize_source_row(json.loads(line)) meta = row["meta"] yield { "request_id": meta["request_id"], "session_id": meta["session_id"], "request_ready_time_ms": meta["request_ready_time_ms"], "request_end_time_ms": meta["request_end_time_ms"], "declared_tool_names": [ tool["name"] for tool in row.get("declared_tools", []) if tool.get("name") ], "raw_messages": row["raw_messages"], } def _count_child_refs_by_chat_id(path, limit=None): counts = Counter() for index, row in enumerate(_iter_release_rows(path), start=1): if limit is not None and index > limit: break parent_chat_id = int(row.get("parent_chat_id", -1) or -1) if parent_chat_id != -1: counts[parent_chat_id] += 1 return counts def _new_block_meta(request_id, ready_ms): return [ready_ms, ready_ms, 0, request_id, request_id, "", 0] def _build_alive_block_timeline_from_events(events): alive_rows = [] alive_count = 0 peak_alive_blocks = 0 for timestamp_ms in sorted(events): alive_count += events[timestamp_ms] peak_alive_blocks = max(peak_alive_blocks, alive_count) alive_rows.append( { "timestamp_ms": timestamp_ms, "delta_alive_blocks": events[timestamp_ms], "alive_block_count": alive_count, } ) return { "peak_alive_blocks": peak_alive_blocks, "event_count": len(alive_rows), }, alive_rows def _compute_prefix_hits( global_store, bucket_store, *, hash_ids, request_id, ready_ms, reuse_gap_counts=None, ): global_prefix_active = True bucket_prefix_active = True global_prefix_match_blocks = 0 bucket_prefix_match_blocks = 0 global_source_request_id = "" bucket_source_request_id = "" for block_id in hash_ids: global_meta = global_store.get(block_id) if global_meta is not None and global_prefix_active: global_prefix_match_blocks += 1 global_source_request_id = global_meta[LAST_REQUEST_ID] if reuse_gap_counts is not None: reuse_gap_counts[max(ready_ms - global_meta[LAST_SEEN_MS], 0)] += 1 global_meta[LAST_REUSE_MS] = ready_ms global_meta[LAST_REUSE_REQUEST_ID] = request_id global_meta[REUSE_COUNT] += 1 elif global_meta is None: global_prefix_active = False global_meta = _new_block_meta(request_id, ready_ms) else: global_prefix_active = False global_meta[LAST_SEEN_MS] = ready_ms global_meta[LAST_REQUEST_ID] = request_id global_store.put(block_id, global_meta) bucket_meta = bucket_store.get(block_id) if bucket_meta is not None and bucket_prefix_active: bucket_prefix_match_blocks += 1 bucket_source_request_id = bucket_meta[LAST_REQUEST_ID] bucket_meta[LAST_REUSE_MS] = ready_ms bucket_meta[LAST_REUSE_REQUEST_ID] = request_id bucket_meta[REUSE_COUNT] += 1 elif bucket_meta is None: bucket_prefix_active = False bucket_meta = _new_block_meta(request_id, ready_ms) else: bucket_prefix_active = False bucket_meta[LAST_SEEN_MS] = ready_ms bucket_meta[LAST_REQUEST_ID] = request_id bucket_store.put(block_id, bucket_meta) return ( global_prefix_match_blocks, global_source_request_id, bucket_prefix_match_blocks, bucket_source_request_id, ) def _iter_release_rows(path): with open(path, "r", encoding="utf-8") as handle: for line in handle: row = json.loads(line) yield { "chat_id": int(row.get("chat_id", -1) or -1), "parent_chat_id": int(row.get("parent_chat_id", -1) or -1), "timestamp": row.get("timestamp"), "turn": int(row.get("turn", 0) or 0), "type": row.get("type", ""), "input_length": int(row.get("input_length", 0) or 0), "output_length": int(row.get("output_length", 0) or 0), "hash_ids": [int(value) for value in row.get("hash_ids", [])], } def _message_signature(message: dict) -> str: return str(message.get("role", "")) def _common_prefix_message_count(previous_messages, current_messages): count = 0 for previous, current in zip(previous_messages, current_messages): if _message_signature(previous) != _message_signature(current): break count += 1 return count def _classify_trigger(previous_messages, current_messages): common_prefix_count = _common_prefix_message_count(previous_messages, current_messages) appended_messages = current_messages[common_prefix_count:] appended_message_count = len(appended_messages) last_role = str(current_messages[-1].get("role", "unknown")) if current_messages else "unknown" trigger_group = last_role trigger_detail = f"last_message_role={last_role}" return { "common_prefix_message_count": common_prefix_count, "appended_message_count": appended_message_count, "first_new_role": str(appended_messages[0].get("role", "unknown")) if appended_messages else "", "trigger_group": trigger_group, "trigger_detail": trigger_detail, } def _bucket_definition_rows(bucket_defs): rows = [] for bucket, lower_bound, upper_bound in bucket_defs: rows.append( { "bucket": bucket, "input_tokens_min_inclusive": lower_bound, "input_tokens_max_exclusive": upper_bound, } ) return rows def _clear_details_dir(details_dir: Path) -> None: details_dir.mkdir(parents=True, exist_ok=True) for path in details_dir.iterdir(): if path.is_file(): path.unlink() def collect_existing_detail_paths(output_dir): details_dir = resolve_details_dir(output_dir) return { "details_dir": details_dir, "progress": details_dir / "progress.json", "request_metrics": details_dir / "request_metrics.csv", "theoretical_block_reuse_gaps": details_dir / "theoretical_block_reuse_gaps.csv", "theoretical_block_lifetimes": details_dir / "theoretical_block_lifetimes.csv", "theoretical_alive_block_timeline": details_dir / "theoretical_alive_block_timeline.csv", "session_bucket_boundary_miss": details_dir / "session_bucket_boundary_miss.csv", "details_summary": details_dir / DETAILS_SUMMARY_FILENAME, } def run_advanced_from_existing( source_path, release_path, features_path, output_dir, input_length_bucket_thresholds=None, show_progress=True, limit=None, ): output_dir = Path(output_dir) details_dir = preferred_details_dir(output_dir) _clear_details_dir(details_dir) source_path = Path(source_path) release_path = Path(release_path) features_path = Path(features_path) total_requests = limit if limit is not None else _count_feature_rows(features_path) release_request_count = _count_lines(release_path) if limit is None and release_request_count != total_requests: raise ValueError( f"release/features row count mismatch: release={release_request_count} vs features={total_requests}" ) process = psutil.Process() peak_rss_mb = 0.0 started_at = time.monotonic() bucket_defs = build_input_length_bucket_defs(input_length_bucket_thresholds) child_ref_counts = _count_child_refs_by_chat_id(release_path, limit=limit) store = InMemoryBlockCache() bucket_stores = {bucket_label: InMemoryBlockCache() for bucket_label, _, _ in bucket_defs} progress_state_path = details_dir / "progress.json" next_progress_flush_at = started_at + PROGRESS_FLUSH_INTERVAL_S request_metrics_path = details_dir / "request_metrics.csv" processed_requests = 0 last_request_id = "" reuse_gap_counts = Counter() bucket_reused_block_totals = Counter() total_prompt_blocks = 0 total_global_reused_blocks = 0 session_last = {} chat_state_for_children = {} session_bucket_totals = { bucket_label: { "edge_count": 0, "reusable_edge_count": 0, "cross_bucket_edge_count": 0, "shared_prefix_units_sum": 0, "cross_bucket_shared_prefix_units_sum": 0, } for bucket_label, _, _ in bucket_defs } with request_metrics_path.open("w", encoding="utf-8", newline="") as request_metrics_handle, features_path.open( "r", encoding="utf-8" ) as features_handle: feature_reader = csv.DictReader(features_handle) source_iter = _read_source_minimal(source_path) release_iter = _iter_release_rows(release_path) if limit is not None: feature_reader = islice(feature_reader, limit) source_iter = islice(source_iter, limit) release_iter = islice(release_iter, limit) request_metrics_writer = None progress = tqdm( total=total_requests, desc="Build details", unit="req", dynamic_ncols=True, disable=not show_progress, ) last_progress_refresh_at = started_at try: for source_row, feature_row, release_row in zip(source_iter, feature_reader, release_iter): request_id = source_row["request_id"] session_id = source_row["session_id"] ready_ms = int(source_row["request_ready_time_ms"]) end_ms = int(source_row["request_end_time_ms"]) tool_names = source_row["declared_tool_names"] raw_messages = source_row["raw_messages"] hash_ids = release_row["hash_ids"] release_input_length = int(release_row["input_length"]) release_output_length = int(release_row["output_length"]) feature_input_tokens = int(feature_row["input_tokens"]) feature_output_tokens = int(feature_row["output_tokens"]) if feature_input_tokens != release_input_length: raise ValueError( f"release/raw mismatch at request {request_id}: " f"features.input_tokens={feature_row['input_tokens']} vs release.input_length={release_input_length}" ) if feature_output_tokens != release_output_length: raise ValueError( f"release/raw mismatch at request {request_id}: " f"features.output_tokens={feature_row['output_tokens']} vs release.output_length={release_output_length}" ) input_tokens = feature_input_tokens bucket_label = assign_input_length_bucket(input_tokens, bucket_defs) bucket_store = bucket_stores[bucket_label] ( prefix_match_blocks, global_source_request_id, bucketed_prefix_match_blocks, bucketed_source_request_id, ) = _compute_prefix_hits( store, bucket_store, hash_ids=hash_ids, request_id=request_id, ready_ms=ready_ms, reuse_gap_counts=reuse_gap_counts, ) prompt_block_count = len(hash_ids) theoretical_prefix_hit_ratio = prefix_match_blocks / prompt_block_count if prompt_block_count else 0.0 bucketed_theoretical_prefix_hit_ratio = ( bucketed_prefix_match_blocks / prompt_block_count if prompt_block_count else 0.0 ) previous_session_state = session_last.get(session_id) trigger = _classify_trigger( previous_session_state["raw_messages"] if previous_session_state is not None else [], raw_messages, ) feature_row["request_ready_time_ms"] = ready_ms feature_row["request_end_time_ms"] = end_ms feature_row["turn"] = release_row["turn"] feature_row["chat_id"] = release_row["chat_id"] feature_row["parent_chat_id"] = release_row["parent_chat_id"] feature_row["trigger_group"] = trigger["trigger_group"] feature_row["trigger_detail"] = trigger["trigger_detail"] feature_row["first_new_role"] = trigger["first_new_role"] feature_row["common_prefix_message_count"] = trigger["common_prefix_message_count"] feature_row["appended_message_count"] = trigger["appended_message_count"] feature_row["input_length_bucket"] = bucket_label feature_row["declared_tool_names"] = ";".join(tool_names) feature_row["theoretical_prompt_unit_length"] = prompt_block_count feature_row["theoretical_prefix_hit_blocks"] = prefix_match_blocks feature_row["theoretical_prefix_hit_ratio"] = theoretical_prefix_hit_ratio feature_row["theoretical_source_request_id"] = global_source_request_id feature_row["bucketed_theoretical_prefix_hit_blocks"] = bucketed_prefix_match_blocks feature_row["bucketed_theoretical_prefix_hit_ratio"] = bucketed_theoretical_prefix_hit_ratio feature_row["bucketed_theoretical_source_request_id"] = bucketed_source_request_id feature_row["theoretical_bucket_boundary_loss_blocks"] = max( prefix_match_blocks - bucketed_prefix_match_blocks, 0, ) feature_row["theoretical_bucket_boundary_loss_ratio"] = ( feature_row["theoretical_bucket_boundary_loss_blocks"] / prompt_block_count if prompt_block_count else 0.0 ) if request_metrics_writer is None: request_metrics_writer = csv.DictWriter( request_metrics_handle, fieldnames=list(feature_row.keys()), ) request_metrics_writer.writeheader() request_metrics_writer.writerow(feature_row) chat_id = release_row["chat_id"] parent_chat_id = release_row["parent_chat_id"] if parent_chat_id != -1: parent_state = chat_state_for_children.get(parent_chat_id) if parent_state is not None: shared_prefix_units = 0 for parent_block_id, child_block_id in zip(parent_state["hash_ids"], hash_ids): if parent_block_id != child_block_id: break shared_prefix_units += 1 bucket_totals = session_bucket_totals[bucket_label] bucket_totals["edge_count"] += 1 if shared_prefix_units > 0: bucket_totals["reusable_edge_count"] += 1 if parent_state["bucket_label"] != bucket_label: bucket_totals["cross_bucket_edge_count"] += 1 bucket_totals["cross_bucket_shared_prefix_units_sum"] += shared_prefix_units bucket_totals["shared_prefix_units_sum"] += shared_prefix_units remaining_children = child_ref_counts.get(parent_chat_id, 0) - 1 if remaining_children > 0: child_ref_counts[parent_chat_id] = remaining_children else: child_ref_counts.pop(parent_chat_id, None) chat_state_for_children.pop(parent_chat_id, None) if chat_id != -1 and child_ref_counts.get(chat_id, 0) > 0: chat_state_for_children[chat_id] = { "bucket_label": bucket_label, "hash_ids": hash_ids, } total_prompt_blocks += prompt_block_count total_global_reused_blocks += prefix_match_blocks bucket_reused_block_totals[bucket_label] += bucketed_prefix_match_blocks session_last[session_id] = { "request_id": request_id, "request_ready_time_ms": ready_ms, "request_end_time_ms": end_ms, "raw_messages": raw_messages, } processed_requests += 1 last_request_id = request_id progress.update(1) now = time.monotonic() should_refresh_progress = ( processed_requests == 1 or processed_requests % PROGRESS_REFRESH_INTERVAL_REQ == 0 or now - last_progress_refresh_at >= PROGRESS_REFRESH_INTERVAL_S or processed_requests == total_requests ) if should_refresh_progress: fraction_done = progress.n / progress.total if progress.total else 0.0 elapsed_s = max(now - started_at, 1e-9) req_per_s = progress.n / elapsed_s eta_s = ((progress.total - progress.n) / req_per_s) if req_per_s > 0 and progress.total else 0.0 total_bucket_state_count = sum(len(each_store) for each_store in bucket_stores.values()) postfix, peak_rss_mb = _progress_postfix( process, peak_rss_mb, fraction_done, req_s=f"{req_per_s:.1f}", eta=_format_duration(eta_s), blocks=len(store), bucket_blocks=total_bucket_state_count, sessions=len(session_last), ) progress.set_postfix(postfix) last_progress_refresh_at = now if processed_requests and now >= next_progress_flush_at: current_rss_mb = process.memory_info().rss / (1024 * 1024) peak_rss_mb = max(peak_rss_mb, current_rss_mb) est_peak_mb = _estimate_peak_rss_mb( current_rss_mb, peak_rss_mb, (processed_requests / total_requests) if total_requests else 1.0, ) _write_progress_state( progress_state_path, total_requests=total_requests, processed_requests=processed_requests, started_at=started_at, current_rss_mb=current_rss_mb, peak_rss_mb=peak_rss_mb, est_peak_mb=est_peak_mb, source_path=f"{source_path} + {release_path}", features_path=features_path, last_request_id=last_request_id, block_state_count=len(store), bucket_state_count=total_bucket_state_count, ) next_progress_flush_at = now + PROGRESS_FLUSH_INTERVAL_S finally: progress.close() theoretical_block_reuse_gaps_path = details_dir / "theoretical_block_reuse_gaps.csv" write_csv( theoretical_block_reuse_gaps_path, [ {"reuse_gap_ms": reuse_gap_ms, "count": count} for reuse_gap_ms, count in sorted(reuse_gap_counts.items()) ], fieldnames=["reuse_gap_ms", "count"], ) theoretical_block_lifetimes_path = details_dir / "theoretical_block_lifetimes.csv" alive_block_events = Counter() block_lifetime_rows = [] for ( block_hash, first_seen_ms, last_seen_ms, last_reuse_ms, first_request_id, last_request_id_for_block, last_reuse_request_id, reuse_count, ) in store.iter_blocks(): lifecycle_end_ms = last_reuse_ms if reuse_count > 0 else first_seen_ms lifetime_ms = max(lifecycle_end_ms - first_seen_ms, 0) block_lifetime_rows.append( { "hash": block_hash, "first_request_id": first_request_id, "last_request_id": last_request_id_for_block, "first_seen_ms": first_seen_ms, "last_seen_ms": last_seen_ms, "last_reuse_ms": last_reuse_ms, "last_reuse_request_id": last_reuse_request_id, "reuse_count": reuse_count, "lifetime_ms": lifetime_ms, "span_end_ms": lifecycle_end_ms, "reused": 1 if reuse_count > 0 else 0, } ) alive_block_events[first_seen_ms] += 1 alive_block_events[lifecycle_end_ms + 1] -= 1 write_csv(theoretical_block_lifetimes_path, block_lifetime_rows) alive_block_timeline_summary, alive_block_timeline_rows = _build_alive_block_timeline_from_events(alive_block_events) theoretical_alive_block_timeline_path = details_dir / "theoretical_alive_block_timeline.csv" write_csv(theoretical_alive_block_timeline_path, alive_block_timeline_rows) session_bucket_boundary_rows = [] for bucket_label, _, _ in bucket_defs: bucket_totals = session_bucket_totals[bucket_label] total_bucket_reused_blocks = bucket_reused_block_totals[bucket_label] session_bucket_boundary_rows.append( { "bucket": bucket_label, "edge_count": bucket_totals["edge_count"], "reusable_edge_count": bucket_totals["reusable_edge_count"], "cross_bucket_edge_count": bucket_totals["cross_bucket_edge_count"], "cross_bucket_edge_fraction": ( bucket_totals["cross_bucket_edge_count"] / bucket_totals["edge_count"] if bucket_totals["edge_count"] else 0.0 ), "shared_prefix_units_sum": bucket_totals["shared_prefix_units_sum"], "cross_bucket_shared_prefix_units_sum": bucket_totals["cross_bucket_shared_prefix_units_sum"], "cross_bucket_shared_prefix_unit_fraction": ( bucket_totals["cross_bucket_shared_prefix_units_sum"] / bucket_totals["shared_prefix_units_sum"] if bucket_totals["shared_prefix_units_sum"] else 0.0 ), "bucket_total_reused_blocks": total_bucket_reused_blocks, "reduced_reused_blocks_ratio": ( bucket_totals["cross_bucket_shared_prefix_units_sum"] / total_bucket_reused_blocks if total_bucket_reused_blocks else 0.0 ), } ) session_bucket_boundary_miss_path = details_dir / "session_bucket_boundary_miss.csv" write_csv(session_bucket_boundary_miss_path, session_bucket_boundary_rows) details_summary_path = details_dir / DETAILS_SUMMARY_FILENAME details_summary = { "schema_version": 3, "request_count": total_requests, "figure_count": 13, "cache_analysis_mode": "release_hash_ids", "release_path": str(release_path), "bucket_definition": {"buckets": _bucket_definition_rows(bucket_defs)}, "global_prompt_blocks": total_prompt_blocks, "global_reused_blocks": total_global_reused_blocks, "global_reuse_ratio": (total_global_reused_blocks / total_prompt_blocks) if total_prompt_blocks else 0.0, "alive_block_timeline_summary": alive_block_timeline_summary, "detail_files": [ "request_metrics.csv", "theoretical_block_reuse_gaps.csv", "theoretical_block_lifetimes.csv", "theoretical_alive_block_timeline.csv", "session_bucket_boundary_miss.csv", DETAILS_SUMMARY_FILENAME, "progress.json", ], } details_summary_path.write_text(json.dumps(details_summary, ensure_ascii=False, indent=2), encoding="utf-8") current_rss_mb = process.memory_info().rss / (1024 * 1024) peak_rss_mb = max(peak_rss_mb, current_rss_mb) est_peak_mb = _estimate_peak_rss_mb(current_rss_mb, peak_rss_mb, 1.0) _write_progress_state( progress_state_path, total_requests=total_requests, processed_requests=processed_requests, started_at=started_at, current_rss_mb=current_rss_mb, peak_rss_mb=peak_rss_mb, est_peak_mb=est_peak_mb, source_path=f"{source_path} + {release_path}", features_path=features_path, last_request_id=last_request_id, block_state_count=len(store), bucket_state_count=sum(len(bucket_store) for bucket_store in bucket_stores.values()), ) return { "details_dir": details_dir, "progress": progress_state_path, "request_metrics": request_metrics_path, "theoretical_block_reuse_gaps": theoretical_block_reuse_gaps_path, "theoretical_block_lifetimes": theoretical_block_lifetimes_path, "theoretical_alive_block_timeline": theoretical_alive_block_timeline_path, "session_bucket_boundary_miss": session_bucket_boundary_miss_path, "details_summary": details_summary_path, }