#!/usr/bin/env python3 """Summarize bounded Runtime record-view profile events without record data.""" from __future__ import annotations import argparse import json import math import re import shlex from collections import defaultdict from pathlib import Path from typing import Any, Iterable EVENTS = { "runtime_record_view_phase_profile", "retrieve_with_access_phase_profile", "runtime_record_view_reference_profile", } RETRIEVE_PHASES = ( "object_gate", "find_one", "read_access", "all_access", "fls", "grant_projection", ) ANSI_ESCAPE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") def percentile(values: list[float], quantile: float) -> float: if not values: return 0.0 ordered = sorted(values) rank = max(0, math.ceil(quantile * len(ordered)) - 1) return ordered[rank] def _console_value(value: str) -> Any: lowered = value.lower() if lowered == "true": return True if lowered == "false": return False try: return int(value) except ValueError: try: return float(value) except ValueError: return value def _console_event(line: str) -> dict[str, Any] | None: fields: dict[str, Any] = {} try: tokens = shlex.split(ANSI_ESCAPE.sub("", line)) except ValueError: return None for token in tokens: if "=" not in token: continue key, value = token.split("=", 1) if key: fields[key] = _console_value(value) return fields if fields.get("event") in EVENTS else None def json_events(lines: Iterable[str]) -> Iterable[dict[str, Any]]: for line in lines: start = line.find("{") value: Any = None if start >= 0: try: value, _ = json.JSONDecoder().raw_decode(line[start:]) except json.JSONDecodeError: pass if value is None: value = _console_event(line) if isinstance(value, dict) and value.get("event") in EVENTS: yield value def numeric_fields(rows: list[dict[str, Any]]) -> list[str]: return sorted( { key for row in rows for key, value in row.items() if isinstance(value, (int, float)) and not isinstance(value, bool) } ) def aggregate(rows: list[dict[str, Any]]) -> dict[str, Any]: result: dict[str, Any] = {"samples": len(rows)} for field in numeric_fields(rows): values = [float(row[field]) for row in rows if isinstance(row.get(field), (int, float))] result[field] = { "sum": round(sum(values), 3), "mean": round(sum(values) / len(values), 3), "p50": round(percentile(values, 0.50), 3), "p90": round(percentile(values, 0.90), 3), "max": round(max(values), 3), } return result def summarize(events: Iterable[dict[str, Any]]) -> dict[str, Any]: groups: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list) for raw_event in events: event = dict(raw_event) name = str(event["event"]) if name == "retrieve_with_access_phase_profile": event["sql"] = sum(float(event.get(f"{phase}_sql", 0)) for phase in RETRIEVE_PHASES) event["sql_elapsed_ms"] = sum( float(event.get(f"{phase}_sql_elapsed_ms", 0)) for phase in RETRIEVE_PHASES ) if name == "runtime_record_view_reference_profile": identity = f"{event.get('source_object', '?')}->{event.get('target_object', '?')}" else: identity = str(event.get("object", "?")) groups[(name, identity)].append(event) return { "schema": "nexuscrm.runtime-record-view-profile-summary/v1", "groups": [ {"event": event, "identity": identity, **aggregate(rows)} for (event, identity), rows in sorted(groups.items()) ], } def markdown(summary: dict[str, Any]) -> str: lines = [ "# Runtime record-view profile", "", "| event | identity | samples | total p50/max ms | SQL count mean | SQL wait p50/max ms |", "|---|---|---:|---:|---:|---:|", ] for group in summary["groups"]: total = group.get("total_elapsed_ms", group.get("elapsed_ms", {})) sql = group.get("sql", group.get("root_sql", {})) sql_wait = group.get("sql_elapsed_ms", group.get("root_sql_elapsed_ms", {})) lines.append( f"| {group['event']} | {group['identity']} | {group['samples']} | " f"{total.get('p50', 0)}/{total.get('max', 0)} | {sql.get('mean', 0)} | " f"{sql_wait.get('p50', 0)}/{sql_wait.get('max', 0)} |" ) retrieve_groups = [ group for group in summary["groups"] if group["event"] == "retrieve_with_access_phase_profile" ] if retrieve_groups: lines.extend([ "", "## RetrieveWithAccess phases", "", "| identity | phase | wall p50/max ms | SQL count mean | SQL wait p50/max ms |", "|---|---|---:|---:|---:|", ]) for group in retrieve_groups: for phase in RETRIEVE_PHASES: wall = group.get(f"{phase}_elapsed_ms", {}) sql = group.get(f"{phase}_sql", {}) wait = group.get(f"{phase}_sql_elapsed_ms", {}) lines.append( f"| {group['identity']} | {phase} | {wall.get('p50', 0)}/{wall.get('max', 0)} | " f"{sql.get('mean', 0)} | {wait.get('p50', 0)}/{wait.get('max', 0)} |" ) return "\n".join(lines) + "\n" def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("log", type=Path) parser.add_argument("--json-output", type=Path) parser.add_argument("--markdown-output", type=Path) args = parser.parse_args() with args.log.open(encoding="utf-8", errors="replace") as handle: summary = summarize(json_events(handle)) encoded = json.dumps(summary, indent=2, sort_keys=True) + "\n" if args.json_output: args.json_output.write_text(encoded, encoding="utf-8") else: print(encoded, end="") if args.markdown_output: args.markdown_output.write_text(markdown(summary), encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())