import argparse
import json
import math
import random
import time
import tracemalloc
from collections import defaultdict
from pathlib import Path
from typing import Any
from rich.console import Console
from rich.table import Table
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Benchmark skimtoken vs tiktoken performance",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-n",
"--num-items",
type=int,
default=None,
help="Number of items to randomly sample from the dataset. If not specified, all items will be processed.",
)
parser.add_argument(
"-d",
"--dataset-path",
type=Path,
default=None,
help="Path to the dataset JSONL file. If not specified, uses default test.jsonl",
)
parser.add_argument(
"-m",
"--method",
type=str,
choices=["simple", "basic", "multilingual", "multilingual_simple"],
default="multilingual_simple",
help="Type of skimtoken import to use",
)
return parser.parse_args()
def load_dataset(path: Path) -> list[dict[str, Any]]:
data: list[dict[str, Any]] = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
if line.strip():
data.append(json.loads(line))
return data
def get_memory_usage() -> float:
current, _ = tracemalloc.get_traced_memory()
return current / 1024 / 1024
def format_ratio(ratio: float) -> str:
if ratio < 1.0:
return f"[green]{ratio:.3f}x[/green]"
else:
return f"[red]{ratio:.3f}x[/red]"
def format_error_rate(error_rate: float) -> str:
if error_rate < 5.0:
return f"[green]{error_rate:.2f}[/green]"
elif error_rate < 10.0:
return f"[yellow]{error_rate:.2f}[/yellow]"
else:
return f"[red]{error_rate:.2f}[/red]"
def benchmark_tiktoken_init() -> dict[str, Any]:
tracemalloc.start()
start_time = time.perf_counter()
import tiktoken
enc = tiktoken.get_encoding("o200k_base")
end_time = time.perf_counter()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return {
"init_time": end_time - start_time,
"init_memory": peak / 1024 / 1024, "encoder": enc,
}
def benchmark_skimtoken_init(method: str) -> dict[str, Any]:
tracemalloc.start()
start_time = time.perf_counter()
if method == "simple":
from skimtoken.simple import estimate_tokens
elif method == "basic":
from skimtoken.basic import estimate_tokens
elif method == "multilingual":
from skimtoken.multilingual import estimate_tokens
elif method == "multilingual_simple":
from skimtoken.multilingual_simple import estimate_tokens
else:
raise ValueError(f"Unknown skimtoken method: {method}")
end_time = time.perf_counter()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return {
"init_time": end_time - start_time,
"init_memory": peak / 1024 / 1024, "estimate_func": estimate_tokens,
}
def benchmark_tiktoken_execution(texts: list[str], enc: Any) -> dict[str, Any]:
tracemalloc.start()
start_time = time.perf_counter()
token_counts: list[int] = []
for text in texts:
tokens = len(enc.encode(text))
token_counts.append(tokens)
end_time = time.perf_counter()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return {
"exec_time": end_time - start_time,
"exec_memory": peak / 1024 / 1024, "token_counts": token_counts,
}
def benchmark_skimtoken_execution(texts: list[str], estimate_func: Any) -> dict[str, Any]:
tracemalloc.start()
start_time = time.perf_counter()
token_counts: list[int] = []
for text in texts:
tokens = estimate_func(text)
token_counts.append(tokens)
end_time = time.perf_counter()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return {
"exec_time": end_time - start_time,
"exec_memory": peak / 1024 / 1024, "token_counts": token_counts,
}
def calculate_rmse(true_counts: list[int], estimated_counts: list[int]) -> float:
if len(true_counts) != len(estimated_counts):
raise ValueError("Lists must have the same length")
if not true_counts:
return 0.0
squared_errors = [(true - est) ** 2 for true, est in zip(true_counts, estimated_counts)]
mean_squared_error = sum(squared_errors) / len(squared_errors)
return math.sqrt(mean_squared_error)
def calculate_error_rate(true_counts: list[int], estimated_counts: list[int]) -> float:
if len(true_counts) != len(estimated_counts):
raise ValueError("Lists must have the same length")
percentage_errors = [
abs(true - est) / true * 100 for true, est in zip(true_counts, estimated_counts) if true > 0
]
return sum(percentage_errors) / len(percentage_errors) if percentage_errors else 0.0
def calculate_category_metrics(
valid_entries: list[dict[str, Any]], tiktoken_counts: list[int], skimtoken_counts: list[int]
) -> dict[str, dict[str, float]]:
category_data: dict[str, dict[str, list[int]]] = defaultdict(
lambda: {"tiktoken": [], "skimtoken": []}
)
for i, entry in enumerate(valid_entries):
category = entry.get("category", "unknown")
category_data[category]["tiktoken"].append(tiktoken_counts[i])
category_data[category]["skimtoken"].append(skimtoken_counts[i])
category_metrics = {}
for category, counts in category_data.items():
if counts["tiktoken"] and counts["skimtoken"]:
rmse = calculate_rmse(counts["tiktoken"], counts["skimtoken"])
error_rate = calculate_error_rate(counts["tiktoken"], counts["skimtoken"])
category_metrics[category] = {
"rmse": rmse,
"error_rate": error_rate,
"count": len(counts["tiktoken"]),
}
return category_metrics
def main() -> None:
args = parse_args()
console = Console()
if args.dataset_path:
dataset_path = args.dataset_path
else:
dataset_path = Path(__file__).parent.parent / "data" / "cc100_samples" / "test.jsonl"
console.print(f"[cyan]Loading dataset from {dataset_path}...[/cyan]")
dataset = load_dataset(dataset_path)
valid_entries = [
entry for entry in dataset if isinstance(entry.get("text"), str) and entry["text"].strip()
]
if args.num_items is not None:
if args.num_items < len(valid_entries):
valid_entries = random.sample(valid_entries, args.num_items)
console.print(f"[yellow]Randomly sampled {args.num_items} items from dataset[/yellow]")
else:
console.print(
f"[yellow]Using all {len(valid_entries)} items (requested {args.num_items})[/yellow]"
)
texts = [entry["text"] for entry in valid_entries]
console.print(f"[green]Loaded {len(texts)} valid entries[/green]\n")
if not texts:
console.print("[red]Error: No valid entries found in the dataset.[/red]")
console.print(
"[yellow]Please ensure the dataset file contains entries with 'text' field.[/yellow]"
)
return
console.print("[bold cyan]Initialization Benchmark[/bold cyan]")
console.print("[cyan]Benchmarking tiktoken initialization...[/cyan]")
tiktoken_init = benchmark_tiktoken_init()
console.print(f"[cyan]Benchmarking skimtoken.{args.method} initialization...[/cyan]")
skimtoken_init = benchmark_skimtoken_init(args.method)
console.print("\n[bold cyan]Execution Benchmark[/bold cyan]")
console.print("[cyan]Benchmarking tiktoken execution...[/cyan]")
tiktoken_exec = benchmark_tiktoken_execution(texts, tiktoken_init["encoder"])
console.print(f"[cyan]Benchmarking skimtoken.{args.method} execution...[/cyan]")
skimtoken_exec = benchmark_skimtoken_execution(texts, skimtoken_init["estimate_func"])
console.print("\n[bold]Results:[/bold]")
total_chars = sum(len(text) for text in texts)
console.print(f"[dim]Total Samples: {len(texts):,}[/dim]")
console.print(f"[dim]Total Characters: {total_chars:,}[/dim]")
rmse = calculate_rmse(tiktoken_exec["token_counts"], skimtoken_exec["token_counts"])
error_rate = calculate_error_rate(tiktoken_exec["token_counts"], skimtoken_exec["token_counts"])
console.print(f"[dim]Mean RMSE: {rmse:.4f} tokens[/dim]")
console.print(f"[dim]Mean Error Rate: {error_rate:.2f}%[/dim]\n")
table = Table(show_header=True, header_style="bold cyan", show_lines=True)
table.add_column("Metric", style="bold")
table.add_column("tiktoken", justify="right")
table.add_column("skimtoken", justify="right")
table.add_column("Ratio", justify="right")
init_time_ratio = skimtoken_init["init_time"] / tiktoken_init["init_time"]
table.add_row(
"Init Time",
f"{tiktoken_init['init_time']:.6f} s",
f"{skimtoken_init['init_time']:.6f} s",
format_ratio(init_time_ratio),
)
init_memory_ratio = skimtoken_init["init_memory"] / tiktoken_init["init_memory"]
table.add_row(
"Init Memory",
f"{tiktoken_init['init_memory']:.4f} MB",
f"{skimtoken_init['init_memory']:.4f} MB",
format_ratio(init_memory_ratio),
end_section=True,
)
exec_time_ratio = skimtoken_exec["exec_time"] / tiktoken_exec["exec_time"]
table.add_row(
"Exec Time",
f"{tiktoken_exec['exec_time']:.6f} s",
f"{skimtoken_exec['exec_time']:.6f} s",
format_ratio(exec_time_ratio),
)
exec_memory_ratio = skimtoken_exec["exec_memory"] / tiktoken_exec["exec_memory"]
table.add_row(
"Exec Memory",
f"{tiktoken_exec['exec_memory']:.4f} MB",
f"{skimtoken_exec['exec_memory']:.4f} MB",
format_ratio(exec_memory_ratio),
end_section=True,
)
total_time_tiktoken = tiktoken_init["init_time"] + tiktoken_exec["exec_time"]
total_time_skimtoken = skimtoken_init["init_time"] + skimtoken_exec["exec_time"]
total_memory_tiktoken = tiktoken_init["init_memory"] + tiktoken_exec["exec_memory"]
total_memory_skimtoken = skimtoken_init["init_memory"] + skimtoken_exec["exec_memory"]
total_time_ratio = total_time_skimtoken / total_time_tiktoken
total_memory_ratio = total_memory_skimtoken / total_memory_tiktoken
table.add_row(
"[bold]Total Time[/bold]",
f"[bold]{total_time_tiktoken:.6f} s[/bold]",
f"[bold]{total_time_skimtoken:.6f} s[/bold]",
f"[bold]{format_ratio(total_time_ratio)}[/bold]",
)
table.add_row(
"[bold]Total Memory[/bold]",
f"[bold]{total_memory_tiktoken:.4f} MB[/bold]",
f"[bold]{total_memory_skimtoken:.4f} MB[/bold]",
f"[bold]{format_ratio(total_memory_ratio)}[/bold]",
)
console.print(table)
category_metrics = calculate_category_metrics(
valid_entries, tiktoken_exec["token_counts"], skimtoken_exec["token_counts"]
)
if category_metrics:
console.print("\n[bold cyan]Category-wise Metrics[/bold cyan]")
category_table = Table(show_header=True, header_style="bold cyan", show_lines=True)
category_table.add_column("Category", style="bold")
category_table.add_column("Count", justify="right")
category_table.add_column("RMSE", justify="right")
category_table.add_column("Error Rate (%)", justify="right")
sorted_categories = sorted(category_metrics.items(), key=lambda x: x[1]["error_rate"])
for category, metrics in sorted_categories:
category_table.add_row(
category,
str(metrics["count"]),
f"{metrics['rmse']:.4f}",
format_error_rate(metrics["error_rate"]),
)
console.print(category_table)
if __name__ == "__main__":
main()