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/hitches.py
1"""Animation hitches lane parser.23Xcode 26 schema `hitches` columns: start, duration (hitch time), process,4is-system, swap-id, label, display, narrative-description. The5narrative-description field carries Apple's own attribution (e.g.6"Potentially expensive app update(s)") which is the highest-signal column.7"""8from __future__ import annotations910from collections import Counter11from pathlib import Path12from typing import Any1314from . import xctrace, xml_utils1516CANDIDATE_SCHEMAS = ("hitches", "animation-hitch", "hitch")1718START_KEYS = ("start", "time", "sample-time")19DURATION_KEYS = ("duration", "hitch-duration", "frame-duration")202122def analyze(23trace_path: Path,24toc_schemas: frozenset[str],25top_n: int = 10,26window: tuple[int, int] | None = None,27run: int = 1,28) -> dict[str, Any]:29schema = _pick_schema(toc_schemas)30if schema is None:31return {32"lane": "hitches",33"available": False,34"notes": ["Animation hitches not present in trace."],35}3637xml_bytes = xctrace.export_schema(trace_path, schema, run=run)38stream = xml_utils.RowStream(xml_bytes)3940events: list[dict] = []41narrative_counts: Counter[str] = Counter()42system_count = 04344for row in stream:45start_ns = _first_int(row, stream, START_KEYS)46duration_ns = _first_int(row, stream, DURATION_KEYS)47if start_ns is None or duration_ns is None:48continue49if not xml_utils.event_overlaps_window(start_ns, start_ns + duration_ns, window):50continue5152process_el = row.get("process")53process = (54xml_utils.extract_process(process_el, stream)55if process_el is not None else None56)5758narrative_el = row.get("narrative-description")59narrative = xml_utils.str_text(stream.resolve(narrative_el)) if narrative_el is not None else None60if narrative:61narrative_counts[narrative] += 16263is_system_el = row.get("is-system")64is_system = _bool_text(stream.resolve(is_system_el)) if is_system_el is not None else None65if is_system:66system_count += 16768events.append({69"start_ns": start_ns,70"end_ns": start_ns + duration_ns,71"duration_ns": duration_ns,72"hitch_duration_ns": duration_ns, # Xcode 26 `duration` == hitch time73"frame_duration_ns": None,74"hitch_duration_ms": round(duration_ns / 1_000_000, 2),75"frame_duration_ms": None,76"start_ms": round(start_ns / 1_000_000, 2),77"process": (process or {}).get("name"),78"narrative": narrative,79"is_system": bool(is_system) if is_system is not None else None,80})8182events.sort(key=lambda e: e["duration_ns"], reverse=True)8384total_hitch_ms = sum(e["hitch_duration_ms"] for e in events)85worst = events[0] if events else None8687per_process: dict[str, int] = {}88for e in events:89key = e["process"] or "unknown"90per_process[key] = per_process.get(key, 0) + 19192top_offenders = [93{94"start_ms": e["start_ms"],95"hitch_duration_ms": e["hitch_duration_ms"],96"frame_duration_ms": e["frame_duration_ms"],97"process": e["process"],98"narrative": e["narrative"],99"is_system": e["is_system"],100}101for e in events[:top_n]102]103104return {105"lane": "hitches",106"available": True,107"schema_used": schema,108"metrics": {109"count": len(events),110"total_hitch_ms": round(total_hitch_ms, 2),111"worst_hitch_ms": worst["hitch_duration_ms"] if worst else 0,112"per_process": per_process,113"system_hitches": system_count,114"app_hitches": len(events) - system_count,115"narrative_breakdown": dict(narrative_counts.most_common()),116},117"top_offenders": top_offenders,118"notes": [],119"_events": events,120}121122123def _pick_schema(available: frozenset[str]) -> str | None:124for s in CANDIDATE_SCHEMAS:125if s in available:126return s127return None128129130def _first_int(row, stream, keys):131for key in keys:132el = row.get(key)133if el is None:134continue135val = xml_utils.int_text(stream.resolve(el))136if val is not None:137return val138return None139140141def _bool_text(elem) -> bool | None:142txt = xml_utils.str_text(elem)143if txt is None:144return None145return txt.strip() in ("1", "true", "True", "YES", "Yes")146