Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/instruments_parser/hangs.py
1"""Hangs lane parser (schema `potential-hangs`).23The schema lacks inline backtraces — stacks come from Time Profiler samples4that overlap each hang's window. Correlation is done later in correlate.py.5"""6from __future__ import annotations78from pathlib import Path9from typing import Any1011from . import xctrace, xml_utils1213PREFERRED_SCHEMAS = ("potential-hangs",)14FALLBACK_SCHEMAS = ("main-thread-hang", "hang", "hangs")151617def analyze(18trace_path: Path,19toc_schemas: frozenset[str],20top_n: int = 10,21window: tuple[int, int] | None = None,22run: int = 1,23) -> dict[str, Any]:24schema = _pick_schema(toc_schemas)25if schema is None:26return {27"lane": "hangs",28"available": False,29"notes": ["Hangs data not present in trace."],30}3132xml_bytes = xctrace.export_schema(trace_path, schema, run=run)33stream = xml_utils.RowStream(xml_bytes)3435hangs: list[dict] = []36for row in stream:37start_el = row.get("start")38dur_el = row.get("duration")39type_el = row.get("hang-type")40thread_el = row.get("thread")41if start_el is None or dur_el is None:42continue43start_ns = xml_utils.int_text(stream.resolve(start_el))44duration_ns = xml_utils.int_text(stream.resolve(dur_el))45if start_ns is None or duration_ns is None:46continue47if not xml_utils.event_overlaps_window(start_ns, start_ns + duration_ns, window):48continue49hang_type = xml_utils.str_text(stream.resolve(type_el)) if type_el is not None else None50thread = xml_utils.extract_thread(thread_el, stream) if thread_el is not None else None5152hangs.append({53"start_ns": start_ns,54"duration_ns": duration_ns,55"end_ns": start_ns + duration_ns,56"duration_ms": round(duration_ns / 1_000_000, 2),57"start_ms": round(start_ns / 1_000_000, 2),58"hang_type": hang_type or "Hang",59"thread": thread,60})6162hangs.sort(key=lambda h: h["duration_ns"], reverse=True)6364total_ms = sum(h["duration_ms"] for h in hangs)65worst = hangs[0] if hangs else None6667# Severity buckets per Apple docs (Microhang: 250ms–500ms, Hang: ≥500ms).68# We bucket by raw duration so the agent can reason about it.69buckets = {"lt_250ms": 0, "250ms_1s": 0, "gt_1s": 0}70for h in hangs:71if h["duration_ms"] < 250:72buckets["lt_250ms"] += 173elif h["duration_ms"] < 1000:74buckets["250ms_1s"] += 175else:76buckets["gt_1s"] += 17778top_offenders = [79{80"start_ms": h["start_ms"],81"duration_ms": h["duration_ms"],82"hang_type": h["hang_type"],83"thread": (h["thread"] or {}).get("name", ""),84}85for h in hangs[:top_n]86]8788return {89"lane": "hangs",90"available": True,91"schema_used": schema,92"metrics": {93"count": len(hangs),94"total_duration_ms": round(total_ms, 2),95"worst_duration_ms": worst["duration_ms"] if worst else 0,96"severity_buckets": buckets,97},98"top_offenders": top_offenders,99"notes": [],100"_events": hangs, # retained for correlation101}102103104def _pick_schema(available: frozenset[str]) -> str | None:105for s in PREFERRED_SCHEMAS + FALLBACK_SCHEMAS:106if s in available:107return s108return None109