Files
2026-07-13 12:24:33 +08:00

424 lines
13 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Analyze chunk hash files generated by the file_hash strategy.
This script reads chunk hash files and provides statistics about chunk reuse patterns.
"""
# Standard
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Set
import argparse
import json
import sys
def load_chunk_hashes(input_dir: Path) -> List[Dict]:
"""Load all chunk hash records from JSONL files.
Args:
input_dir: Directory containing chunk hash JSONL files
Returns:
List of chunk hash records
"""
records: List[Dict] = []
hash_files = sorted(input_dir.glob("chunk_hashes_*.jsonl"))
if not hash_files:
print(f"No chunk hash files found in {input_dir}")
return records
print(f"Found {len(hash_files)} chunk hash files")
for hash_file in hash_files:
print(f"Reading {hash_file.name}...")
with open(hash_file) as f:
for line in f:
try:
data = json.loads(line)
records.append(data)
except json.JSONDecodeError as e:
print(f"Warning: Failed to parse line in {hash_file}: {e}")
continue
return records
def analyze_chunk_hashes(records: List[Dict], top_n: int = 10) -> Dict:
"""Analyze chunk hash records and compute statistics.
Args:
records: List of chunk hash records
top_n: Number of top reused chunks to include
Returns:
Dictionary containing analysis results
"""
all_hashes: List[str] = []
unique_hashes: Set[str] = set()
lookup_ids: Set[str] = set()
hash_frequency: Counter = Counter()
request_stats: Dict[str, Dict] = {}
for record in records:
lookup_id = record.get("lookup_id", "unknown")
chunk_hashes = record.get("chunk_hashes", [])
lookup_ids.add(lookup_id)
all_hashes.extend(chunk_hashes)
unique_hashes.update(chunk_hashes)
hash_frequency.update(chunk_hashes)
if lookup_id not in request_stats:
request_stats[lookup_id] = {
"chunk_count": len(chunk_hashes),
"timestamp": record.get("timestamp", 0),
}
total_chunks = len(all_hashes)
unique_chunks = len(unique_hashes)
duplicate_chunks = total_chunks - unique_chunks
reuse_rate = duplicate_chunks / total_chunks if total_chunks > 0 else 0.0
# Find most frequently reused chunks
top_reused = hash_frequency.most_common(top_n)
# Per-request statistics
chunk_counts = [stats["chunk_count"] for stats in request_stats.values()]
avg_chunks = sum(chunk_counts) / len(chunk_counts) if chunk_counts else 0
max_chunks = max(chunk_counts) if chunk_counts else 0
min_chunks = min(chunk_counts) if chunk_counts else 0
# Frequency distribution
reuse_counts = list(hash_frequency.values())
single_use = sum(1 for c in reuse_counts if c == 1)
multi_use = len(reuse_counts) - single_use
return {
"total_records": len(records),
"total_chunks": total_chunks,
"unique_chunks": unique_chunks,
"duplicate_chunks": duplicate_chunks,
"reuse_rate": reuse_rate,
"unique_lookup_ids": len(lookup_ids),
"top_reused_chunks": top_reused,
"request_stats": {
"total_requests": len(request_stats),
"avg_chunks_per_request": avg_chunks,
"max_chunks_per_request": max_chunks,
"min_chunks_per_request": min_chunks,
},
"frequency_distribution": {
"single_use_chunks": single_use,
"multi_use_chunks": multi_use,
"single_use_percentage": single_use / len(reuse_counts)
if reuse_counts
else 0,
"multi_use_percentage": multi_use / len(reuse_counts)
if reuse_counts
else 0,
},
}
def print_analysis(analysis: Dict, verbose: bool = False) -> None:
"""Print analysis results in a readable format.
Args:
analysis: Analysis results dictionary
verbose: Whether to print detailed statistics
"""
print("\n" + "=" * 60)
print("Chunk Hash Analysis Results")
print("=" * 60)
print("\nBasic Statistics:")
print(f" Total records: {analysis['total_records']:,}")
print(f" Unique lookup IDs: {analysis['unique_lookup_ids']:,}")
print(f" Total chunks: {analysis['total_chunks']:,}")
print(f" Unique chunks: {analysis['unique_chunks']:,}")
print(f" Duplicate chunks: {analysis['duplicate_chunks']:,}")
print(f" Reuse rate: {analysis['reuse_rate']:.2%}")
if verbose:
req_stats = analysis["request_stats"]
print("\nPer-Request Statistics:")
print(f" Total requests: {req_stats['total_requests']:,}")
print(f" Avg chunks/request: {req_stats['avg_chunks_per_request']:.2f}")
print(f" Max chunks/request: {req_stats['max_chunks_per_request']:,}")
print(f" Min chunks/request: {req_stats['min_chunks_per_request']:,}")
freq_dist = analysis["frequency_distribution"]
print("\nFrequency Distribution:")
single_pct = freq_dist["single_use_percentage"]
multi_pct = freq_dist["multi_use_percentage"]
print(
f" Single-use chunks: "
f"{freq_dist['single_use_chunks']:,} ({single_pct:.2%})"
)
print(
f" Multi-use chunks: "
f"{freq_dist['multi_use_chunks']:,} ({multi_pct:.2%})"
)
top_n = len(analysis["top_reused_chunks"])
print(f"\nTop {top_n} Most Reused Chunks:")
for i, (chunk_hash, count) in enumerate(analysis["top_reused_chunks"], 1):
print(f" {i:2d}. {chunk_hash[:16]}... (reused {count:,} times)")
print("\n" + "=" * 60)
def analyze_time_series(records: List[Dict]) -> Dict:
"""Analyze chunk distribution over time.
Args:
records: List of chunk hash records
Returns:
Dictionary containing time-series statistics
"""
hourly_stats: Dict = defaultdict(lambda: {"total": 0, "unique": set()})
for record in records:
timestamp = record.get("timestamp", 0)
if timestamp > 0:
hour = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:00")
chunk_hashes = record.get("chunk_hashes", [])
hourly_stats[hour]["total"] += len(chunk_hashes)
hourly_stats[hour]["unique"].update(chunk_hashes)
# Convert sets to counts for JSON serialization
time_series = {}
for hour, stats in sorted(hourly_stats.items()):
total = stats["total"]
unique = len(stats["unique"])
reuse_rate = (total - unique) / total if total > 0 else 0.0
time_series[hour] = {
"total_chunks": total,
"unique_chunks": unique,
"reuse_rate": reuse_rate,
}
return time_series
def estimate_memory(
unique_chunks: int, chunk_size: int = 256, bytes_per_token: int = 2
) -> Dict:
"""Estimate memory requirements for caching.
Args:
unique_chunks: Number of unique chunks
chunk_size: Number of tokens per chunk
bytes_per_token: Bytes per token
Returns:
Dictionary containing memory estimates
"""
bytes_per_chunk = chunk_size * bytes_per_token
total_memory_bytes = unique_chunks * bytes_per_chunk
total_memory_mb = total_memory_bytes / (1024 * 1024)
total_memory_gb = total_memory_mb / 1024
return {
"unique_chunks": unique_chunks,
"chunk_size_tokens": chunk_size,
"bytes_per_token": bytes_per_token,
"bytes_per_chunk": bytes_per_chunk,
"total_memory_bytes": total_memory_bytes,
"total_memory_mb": total_memory_mb,
"total_memory_gb": total_memory_gb,
}
def export_results(
analysis: Dict,
output_file: Path,
include_time_series: bool = False,
records: Optional[List[Dict]] = None,
) -> None:
"""Export analysis results to a JSON file.
Args:
analysis: Analysis results dictionary
output_file: Output file path
include_time_series: Whether to include time-series analysis
records: Original records (needed for time-series analysis)
"""
# Convert Counter objects to lists for JSON serialization
export_data = analysis.copy()
export_data["top_reused_chunks"] = [
{"hash": h, "count": c} for h, c in analysis["top_reused_chunks"]
]
# Add memory estimation
export_data["memory_estimation"] = estimate_memory(analysis["unique_chunks"])
# Add time-series analysis if requested
if include_time_series and records:
export_data["time_series"] = analyze_time_series(records)
with open(output_file, "w") as f:
json.dump(export_data, f, indent=2)
print(f"\nResults exported to {output_file}")
def print_time_series(time_series: Dict) -> None:
"""Print time-series analysis results.
Args:
time_series: Time-series statistics dictionary
"""
if not time_series:
print("\nNo time-series data available")
return
print("\n" + "=" * 70)
print("Time-Series Analysis")
print("=" * 70)
header = (
f"\n{'Hour':<20} | {'Total Chunks':>12} | "
f"{'Unique Chunks':>13} | {'Reuse Rate':>10}"
)
print(header)
print("-" * 70)
for hour, stats in sorted(time_series.items()):
row = (
f"{hour:<20} | {stats['total_chunks']:>12,} | "
f"{stats['unique_chunks']:>13,} | {stats['reuse_rate']:>9.2%}"
)
print(row)
print("=" * 70)
def print_memory_estimation(memory_est: Dict) -> None:
"""Print memory estimation results.
Args:
memory_est: Memory estimation dictionary
"""
print("\n" + "=" * 60)
print("Memory Estimation")
print("=" * 60)
print(f"\n Unique chunks: {memory_est['unique_chunks']:,}")
print(f" Chunk size: {memory_est['chunk_size_tokens']} tokens")
print(f" Bytes per token: {memory_est['bytes_per_token']} bytes")
print(f" Bytes per chunk: {memory_est['bytes_per_chunk']:,} bytes")
mem_mb = memory_est["total_memory_mb"]
mem_gb = memory_est["total_memory_gb"]
print(f" Total memory: {mem_mb:.2f} MB ({mem_gb:.2f} GB)")
print("=" * 60)
def main():
parser = argparse.ArgumentParser(
description="Analyze chunk hash files from LMCache file_hash strategy"
)
parser.add_argument(
"--input-dir",
type=Path,
default=Path("./chunk_hashes"),
help=("Directory containing chunk hash JSONL files (default: ./chunk_hashes)"),
)
parser.add_argument(
"--output",
type=Path,
help="Output JSON file for analysis results (optional)",
)
parser.add_argument(
"--top-n",
type=int,
default=10,
help="Number of top reused chunks to display (default: 10)",
)
parser.add_argument(
"--verbose",
action="store_true",
help=(
"Show detailed statistics including per-request and frequency distribution"
),
)
parser.add_argument(
"--time-series",
action="store_true",
help="Include time-series analysis (hourly breakdown)",
)
parser.add_argument(
"--memory-estimation",
action="store_true",
help="Show memory estimation for caching unique chunks",
)
parser.add_argument(
"--chunk-size",
type=int,
default=256,
help=("Chunk size in tokens for memory estimation (default: 256)"),
)
parser.add_argument(
"--bytes-per-token",
type=int,
default=2,
help="Bytes per token for memory estimation (default: 2)",
)
args = parser.parse_args()
if not args.input_dir.exists():
print(f"Error: Input directory {args.input_dir} does not exist")
return 1
if not args.input_dir.is_dir():
print(f"Error: {args.input_dir} is not a directory")
return 1
# Load and analyze chunk hashes
print(f"Loading chunk hashes from {args.input_dir}...")
records = load_chunk_hashes(args.input_dir)
if not records:
print("No records found to analyze")
return 1
print(f"Analyzing {len(records)} records...")
analysis = analyze_chunk_hashes(records, top_n=args.top_n)
# Print results
print_analysis(analysis, verbose=args.verbose)
# Time-series analysis
if args.time_series:
time_series = analyze_time_series(records)
print_time_series(time_series)
# Memory estimation
if args.memory_estimation:
memory_est = estimate_memory(
analysis["unique_chunks"],
chunk_size=args.chunk_size,
bytes_per_token=args.bytes_per_token,
)
print_memory_estimation(memory_est)
# Export results if requested
if args.output:
export_results(
analysis,
args.output,
include_time_series=args.time_series,
records=records if args.time_series else None,
)
return 0
if __name__ == "__main__":
sys.exit(main())