from __future__ import annotations import json import time from contextlib import ContextDecorator from dataclasses import dataclass from pathlib import Path from typing import Optional @dataclass class TimerRecord: name: str total: float = 0.0 count: int = 0 @property def avg(self) -> float: return self.total / max(1, self.count) class Timer(ContextDecorator): """ It can be used as: timer = Timer(sync_cuda=True) with timer("sample"): images = sample() timer.print_summary() timer.save_json("timing.json") """ def __init__( self, name: Optional[str] = None, records: Optional[dict[str, TimerRecord]] = None, enabled: bool = True, sync_cuda: bool = True, verbose: bool = False, ): self.name = name self.records = records if records is not None else {} self.enabled = enabled self.sync_cuda = sync_cuda self.verbose = verbose self._start_time: Optional[float] = None def __call__(self, name: str): return Timer( name=name, records=self.records, enabled=self.enabled, sync_cuda=self.sync_cuda, verbose=self.verbose, ) def __enter__(self): if not self.enabled: return self self._sync() self._start_time = time.perf_counter() return self def __exit__(self, exc_type, exc, tb): if not self.enabled: return False self._sync() if self._start_time is None: elapsed = 0.0 else: elapsed = time.perf_counter() - self._start_time name = self.name or "unnamed" if name not in self.records: self.records[name] = TimerRecord(name=name) record = self.records[name] record.total += elapsed record.count += 1 if self.verbose: print(f"[timer] {name}: {elapsed:.4f}s", flush=True) return False def reset(self) -> None: self.records.clear() def get(self, name: str) -> TimerRecord: return self.records[name] def summary(self) -> dict[str, dict[str, float | int]]: return { name: { "total_sec": record.total, "count": record.count, "avg_sec": record.avg, } for name, record in self.records.items() } def save_json(self, path: str | Path) -> None: path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump(self.summary(), f, indent=2) def print_summary(self, sort_by: str = "total", title: str = "Timing summary") -> None: if not self.records: print(f"{title}: no records", flush=True) return if sort_by == "total": key_fn = lambda item: item[1].total elif sort_by == "avg": key_fn = lambda item: item[1].avg elif sort_by == "count": key_fn = lambda item: item[1].count else: raise ValueError("sort_by must be 'total', 'avg', or 'count'.") items = sorted(self.records.items(), key=key_fn, reverse=True) print("=" * 72, flush=True) print(title, flush=True) print("=" * 72, flush=True) print(f"{'operation':36s} {'count':>8s} {'total(s)':>12s} {'avg(s)':>12s}", flush=True) print("-" * 72, flush=True) for name, record in items: print( f"{name:36s} {record.count:8d} {record.total:12.4f} {record.avg:12.4f}", flush=True, ) print("=" * 72, flush=True) def _sync(self) -> None: if not self.sync_cuda: return try: import torch if torch.cuda.is_available(): torch.cuda.synchronize() except ImportError: pass