Files
playbook/antigravity-awesome-skills/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py
T
2026-07-01 16:02:41 +00:00

109 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Hangs lane parser (schema `potential-hangs`).
The schema lacks inline backtraces — stacks come from Time Profiler samples
that overlap each hang's window. Correlation is done later in correlate.py.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from . import xctrace, xml_utils
PREFERRED_SCHEMAS = ("potential-hangs",)
FALLBACK_SCHEMAS = ("main-thread-hang", "hang", "hangs")
def analyze(
trace_path: Path,
toc_schemas: frozenset[str],
top_n: int = 10,
window: tuple[int, int] | None = None,
run: int = 1,
) -> dict[str, Any]:
schema = _pick_schema(toc_schemas)
if schema is None:
return {
"lane": "hangs",
"available": False,
"notes": ["Hangs data not present in trace."],
}
xml_bytes = xctrace.export_schema(trace_path, schema, run=run)
stream = xml_utils.RowStream(xml_bytes)
hangs: list[dict] = []
for row in stream:
start_el = row.get("start")
dur_el = row.get("duration")
type_el = row.get("hang-type")
thread_el = row.get("thread")
if start_el is None or dur_el is None:
continue
start_ns = xml_utils.int_text(stream.resolve(start_el))
duration_ns = xml_utils.int_text(stream.resolve(dur_el))
if start_ns is None or duration_ns is None:
continue
if not xml_utils.event_overlaps_window(start_ns, start_ns + duration_ns, window):
continue
hang_type = xml_utils.str_text(stream.resolve(type_el)) if type_el is not None else None
thread = xml_utils.extract_thread(thread_el, stream) if thread_el is not None else None
hangs.append({
"start_ns": start_ns,
"duration_ns": duration_ns,
"end_ns": start_ns + duration_ns,
"duration_ms": round(duration_ns / 1_000_000, 2),
"start_ms": round(start_ns / 1_000_000, 2),
"hang_type": hang_type or "Hang",
"thread": thread,
})
hangs.sort(key=lambda h: h["duration_ns"], reverse=True)
total_ms = sum(h["duration_ms"] for h in hangs)
worst = hangs[0] if hangs else None
# Severity buckets per Apple docs (Microhang: 250ms500ms, Hang: ≥500ms).
# We bucket by raw duration so the agent can reason about it.
buckets = {"lt_250ms": 0, "250ms_1s": 0, "gt_1s": 0}
for h in hangs:
if h["duration_ms"] < 250:
buckets["lt_250ms"] += 1
elif h["duration_ms"] < 1000:
buckets["250ms_1s"] += 1
else:
buckets["gt_1s"] += 1
top_offenders = [
{
"start_ms": h["start_ms"],
"duration_ms": h["duration_ms"],
"hang_type": h["hang_type"],
"thread": (h["thread"] or {}).get("name", ""),
}
for h in hangs[:top_n]
]
return {
"lane": "hangs",
"available": True,
"schema_used": schema,
"metrics": {
"count": len(hangs),
"total_duration_ms": round(total_ms, 2),
"worst_duration_ms": worst["duration_ms"] if worst else 0,
"severity_buckets": buckets,
},
"top_offenders": top_offenders,
"notes": [],
"_events": hangs, # retained for correlation
}
def _pick_schema(available: frozenset[str]) -> str | None:
for s in PREFERRED_SCHEMAS + FALLBACK_SCHEMAS:
if s in available:
return s
return None