oct
Loading...
Searching...
No Matches
report.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/timing/report.py
4
5"""
6Purpose
7-------
8Report stage and session timings against host baselines.
9
10Responsibilities
11----------------
12- Keep local timing data bounded, private and advisory.
13
14Diagnostics
15-----------
16Domain: GENERAL
17Levels:
18 L1 -- instrumentation failures
19 L2 -- lifecycle
20 L3 -- semantic details
21 L4 -- deep tracing
22
23Contracts
24---------
25- Timing instrumentation never changes the wrapped operation's outcome.
26- Only the fixed timing schema is persisted; command arguments are never stored.
27
28Dependencies
29------------
30- oc_diagnostics (structured timing diagnostics)
31- oct.core.octrc (load_octrc_safe for the project's tier budget overrides)
32- oct.timing.ledger (read_records for the recorded stage timings)
33- oct.timing.tiers (each stage's tier and effective tier budget)
34- oct.tools.atomic_write (atomic_write_text for saving the host timing baseline)
35"""
36from __future__ import annotations
37
38from collections import defaultdict
39import json
40import math
41import os
42from pathlib import Path
43import socket
44from statistics import median
45from datetime import datetime
46from oc_diagnostics import _dbg
47from oct.core.octrc import load_octrc_safe
48from oct.timing.ledger import read_records
49from oct.timing.tiers import budget_for, budgets_ms, tier_for
50from oct.tools.atomic_write import atomic_write_text
51
52def statistics(values: list[float]) -> dict:
53 """Return median and nearest-rank p95 (including small samples)."""
54 ordered = sorted(values)
55 return {"median_ms": median(ordered), "p95_ms": ordered[math.ceil(0.95 * len(ordered)) - 1], "count": len(ordered)}
56
57def _load_baseline(root: Path) -> dict:
58 """Read baseline strictly so a malformed file cannot be overwritten."""
59 path = root / ".option_c" / "timing_baseline.json"
60 if not path.exists():
61 return {"hosts": {}}
62 data = json.loads(path.read_text(encoding="utf-8"))
63 if not isinstance(data, dict) or not isinstance(data.get("hosts"), dict):
64 raise ValueError("invalid timing baseline")
65 return data
66
67def timing_report(root: Path, last: int = 20) -> dict:
68 """Aggregate the current host and CPU count, excluding deliberate waits."""
69 func = "timing_report"
70 _dbg("GENERAL", func, "aggregate timing records", 3)
71 host, cpus = socket.gethostname(), os.cpu_count() or 1
72 groups = defaultdict(list)
73 for row in read_records(root):
74 value = row.get("wall_ms")
75 if row.get("host") == host and row.get("cpus") == cpus and row.get("kind") == "stage" and isinstance(row.get("stage"), str) and type(value) in (int, float) and math.isfinite(value) and value >= 0:
76 groups[row["stage"]].append(value)
77 baseline = _load_baseline(root)
78 entry = baseline["hosts"].get(host, {}).get(str(cpus))
79 timing = load_octrc_safe(root).get("timing")
80 tier_budgets = budgets_ms(timing.get("budgets_ms") if isinstance(timing, dict) else None)
81 stages = {}
82 for stage, values in sorted(groups.items()):
83 numbers = statistics(values[-last:])
84 notes = []
85 reference = (entry or {}).get("stages", {}).get(stage)
86 if reference:
87 tolerance = reference.get("tolerance", (entry or {}).get("tolerance", 25))
88 if numbers["median_ms"] > reference["median_ms"] * (1 + tolerance / 100):
89 notes.append("median exceeds baseline tolerance")
90 budget = (entry or {}).get("budgets", baseline.get("budgets", {})).get(stage)
91 if budget is not None and numbers["median_ms"] > budget:
92 notes.append("median exceeds budget")
93 tier_budget = budget_for(stage, tier_budgets)
94 stages[stage] = {**numbers, "baseline_median_ms": reference["median_ms"] if reference else None, "advisories": notes,
95 "tier": tier_for(stage), "budget_ms": tier_budget,
96 "note": "over budget" if tier_budget is not None and numbers["median_ms"] > tier_budget else ""}
97 return {"host": host, "cpus": cpus, "last": last, "baseline_status": "matched" if entry is not None else "no baseline for this host", "stages": stages}
98
99def write_baseline(root: Path, report: dict, *, force_host: bool = False) -> None:
100 """Preserve all hosts; require acknowledgment before adding a foreign host."""
101 func = "write_baseline"
102 baseline = _load_baseline(root)
103 hosts = baseline["hosts"]
104 host, cpus = report["host"], str(report["cpus"])
105 if hosts and cpus not in hosts.get(host, {}) and not force_host:
106 raise ValueError("baseline belongs to another host or CPU count; use --force-host")
107 entry = hosts.setdefault(host, {}).setdefault(cpus, {})
108 entry.setdefault("tolerance", 25)
109 entry["stages"] = {stage: {"median_ms": values["median_ms"], "count": values["count"]} for stage, values in report["stages"].items()}
110 path = root / ".option_c" / "timing_baseline.json"
111 path.parent.mkdir(parents=True, exist_ok=True)
112 atomic_write_text(path, json.dumps(baseline, indent=2, sort_keys=True) + "\n", encoding="utf-8")
113 _dbg("GENERAL", func, "wrote host timing baseline", 3)
114
115def session_report(path: Path) -> dict:
116 """Pair calls by session and call ID and sum token samples per turn."""
117 func = "session_report"
118 starts = {}
119 tools = defaultdict(list)
120 turns = {}
121 seen = set()
122 for line in path.read_text(encoding="utf-8").splitlines():
123 try:
124 event = json.loads(line)
125 if not isinstance(event, dict):
126 continue
127 identity = event.get("event_id")
128 if identity and identity in seen:
129 continue
130 if identity:
131 seen.add(identity)
132 payload = event.get("payload", {})
133 kind = event.get("kind")
134 key = (event.get("session_id"), event.get("tool_call_id") or payload.get("tool_call_id"))
135 if kind == "tool_call_started":
136 starts[key] = (datetime.fromisoformat(event["ts"]), payload["tool_name"])
137 elif kind == "tool_call_completed" and key in starts:
138 started, name = starts.pop(key)
139 elapsed = (datetime.fromisoformat(event["ts"]) - started).total_seconds() * 1000
140 if elapsed >= 0:
141 tools[name].append(elapsed)
142 elif kind == "provider_budget_consumed":
143 turn = event.get("turn_id") or "unknown"
144 numbers = turns.setdefault(turn, dict(input_tokens=0, output_tokens=0, total_tokens=0))
145 for field in ("input_tokens", "output_tokens"):
146 value = payload.get(field, 0)
147 if type(value) is int and value >= 0:
148 numbers[field] += value
149 numbers["total_tokens"] += value
150 except (ValueError, KeyError, TypeError, AttributeError):
151 # OCT-LINT: disable=no_hardcoded_secrets reason="Fixed diagnostic message; no credential material"
152 _dbg("GENERAL", func, "VALIDATION_ERROR: invalid session timing row", 3)
153 return {"tools": {name: statistics(values) for name, values in sorted(tools.items())}, "turns": turns}
154
155def render_report(report: dict) -> str:
156 """Render precisely the same statistics exposed in JSON."""
157 lines = ["Timing", report["baseline_status"], "stage: median ms | p95 ms | count | baseline median ms | tier | budget ms | note"]
158 for stage, values in report["stages"].items():
159 lines.append(f"{stage}: {values['median_ms']:g} | {values['p95_ms']:g} | {values['count']} | {values['baseline_median_ms']} | {values['tier']} | {values['budget_ms']} | {values['note']}")
160 lines.extend(f" advisory: {note}" for note in values["advisories"])
161 session = report.get("session", {})
162 if session:
163 lines.append("Session tools: median ms | p95 ms | count")
164 for name, values in session["tools"].items():
165 lines.append(f"{name}: {values['median_ms']:g} | {values['p95_ms']:g} | {values['count']}")
166 lines.append("Tokens per turn: input | output | total")
167 for turn, values in session["turns"].items():
168 lines.append(f"{turn}: {values['input_tokens']} | {values['output_tokens']} | {values['total_tokens']}")
169 return "\n".join(lines)
str render_report(dict report)
Definition report.py:155
dict _load_baseline(Path root)
Definition report.py:57
dict session_report(Path path)
Definition report.py:115
dict statistics(list[float] values)
Definition report.py:52
dict timing_report(Path root, int last=20)
Definition report.py:67
None write_baseline(Path root, dict report, *, bool force_host=False)
Definition report.py:99