1from __future__ import annotations2import hashlib, json, os, time, datetime as _dt, subprocess3from pathlib import Path4from typing import Any56ROOT = Path(__file__).resolve().parents[2]78def utc_now() -> str:9 return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")1011def sha256_bytes(b: bytes) -> str:12 return hashlib.sha256(b).hexdigest()1314def sha256_text(s: str) -> str:15 return sha256_bytes(s.encode("utf-8"))1617def sha256_file(p: str | Path, chunk: int = 1 << 20) -> str:18 h = hashlib.sha256()19 with open(p, "rb") as f:20 while True:21 b = f.read(chunk)22 if not b:23 break24 h.update(b)25 return h.hexdigest()2627def sha256_json(obj: Any) -> str:28 return sha256_text(json.dumps(obj, sort_keys=True, separators=(",", ":"), default=str))2930def git_commit(root: Path = ROOT) -> str | None:31 try:32 return subprocess.check_output(["git", "-C", str(root), "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL).strip()33 except Exception:34 return None3536def append_jsonl(path: str | Path, rec: dict) -> None:37 path = Path(path)38 path.parent.mkdir(parents=True, exist_ok=True)39 line = json.dumps(rec, ensure_ascii=False, default=str) + "\n"40 with open(path, "a", encoding="utf-8") as f:41 f.write(line)42 f.flush()43 os.fsync(f.fileno())4445def read_jsonl(path: str | Path) -> list[dict]:46 out = []47 p = Path(path)48 if not p.exists():49 return out50 with open(p, encoding="utf-8") as f:51 for line in f:52 line = line.strip()53 if line:54 out.append(json.loads(line))55 return out5657def write_json(path: str | Path, obj: Any) -> None:58 path = Path(path)59 path.parent.mkdir(parents=True, exist_ok=True)60 with open(path, "w", encoding="utf-8") as f:61 json.dump(obj, f, indent=2, ensure_ascii=False, default=str)6263def read_json(path: str | Path) -> Any:64 with open(path, encoding="utf-8") as f:65 return json.load(f)6667class Timer:68 """Wall-clock span timer (perf_counter). Call sync() hooks around spans for accelerators."""69 def __init__(self):70 self.spans: dict[str, float] = {}71 self._t0: dict[str, float] = {}72 def start(self, name: str):73 self._t0[name] = time.perf_counter()74 def stop(self, name: str) -> float:75 dt = (time.perf_counter() - self._t0.pop(name)) * 1000.076 self.spans[name] = self.spans.get(name, 0.0) + dt77 return dt78 def ms(self) -> dict[str, float]:79 return dict(self.spans)80