claudecode-omc 5.6.7 → 5.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
  2. package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
  3. package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
  4. package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
  5. package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
  6. package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
  7. package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
  8. package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
  9. package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
  10. package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
  11. package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
  12. package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
  13. package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
  14. package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  15. package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  16. package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  17. package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  18. package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
  19. package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
  20. package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  21. package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
  22. package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  23. package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
  24. package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
  25. package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  26. package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  27. package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  28. package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  29. package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  30. package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  31. package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
  32. package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
  33. package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
  34. package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
  35. package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
  36. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
  37. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
  38. package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
  39. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
  40. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
  41. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
  42. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
  43. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
  44. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
  45. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
  46. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
  47. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
  48. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
  49. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
  50. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
  51. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
  52. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
  53. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
  54. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
  55. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
  56. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
  57. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
  58. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
  59. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
  60. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
  61. package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
  62. package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
  63. package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
  64. package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
  65. package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
  66. package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
  67. package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
  68. package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  69. package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  70. package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
  71. package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
  72. package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  73. package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  74. package/.local/skills/swiftui-pro/SKILL.md +108 -0
  75. package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
  76. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
  77. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
  78. package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
  79. package/.local/skills/swiftui-pro/references/api.md +39 -0
  80. package/.local/skills/swiftui-pro/references/data.md +43 -0
  81. package/.local/skills/swiftui-pro/references/design.md +32 -0
  82. package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
  83. package/.local/skills/swiftui-pro/references/navigation.md +14 -0
  84. package/.local/skills/swiftui-pro/references/performance.md +46 -0
  85. package/.local/skills/swiftui-pro/references/swift.md +56 -0
  86. package/.local/skills/swiftui-pro/references/views.md +36 -0
  87. package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
  88. package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
  89. package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  90. package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  91. package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  92. package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
  93. package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  94. package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
  95. package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
  96. package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
  97. package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  98. package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  99. package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  100. package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
  101. package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  102. package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  103. package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  104. package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
  105. package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  106. package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  107. package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  108. package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
  109. package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
  110. package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  111. package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  112. package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  113. package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  114. package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  115. package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  116. package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
  117. package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  118. package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
  119. package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
  120. package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
  121. package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
  122. package/bundled/manifest.json +1 -1
  123. package/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ """Animation hitches lane parser.
2
+
3
+ Xcode 26 schema `hitches` columns: start, duration (hitch time), process,
4
+ is-system, swap-id, label, display, narrative-description. The
5
+ narrative-description field carries Apple's own attribution (e.g.
6
+ "Potentially expensive app update(s)") which is the highest-signal column.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from collections import Counter
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from . import xctrace, xml_utils
15
+
16
+ CANDIDATE_SCHEMAS = ("hitches", "animation-hitch", "hitch")
17
+
18
+ START_KEYS = ("start", "time", "sample-time")
19
+ DURATION_KEYS = ("duration", "hitch-duration", "frame-duration")
20
+
21
+
22
+ def analyze(
23
+ trace_path: Path,
24
+ toc_schemas: frozenset[str],
25
+ top_n: int = 10,
26
+ window: tuple[int, int] | None = None,
27
+ run: int = 1,
28
+ ) -> dict[str, Any]:
29
+ schema = _pick_schema(toc_schemas)
30
+ if schema is None:
31
+ return {
32
+ "lane": "hitches",
33
+ "available": False,
34
+ "notes": ["Animation hitches not present in trace."],
35
+ }
36
+
37
+ xml_bytes = xctrace.export_schema(trace_path, schema, run=run)
38
+ stream = xml_utils.RowStream(xml_bytes)
39
+
40
+ events: list[dict] = []
41
+ narrative_counts: Counter[str] = Counter()
42
+ system_count = 0
43
+
44
+ for row in stream:
45
+ start_ns = _first_int(row, stream, START_KEYS)
46
+ duration_ns = _first_int(row, stream, DURATION_KEYS)
47
+ if start_ns is None or duration_ns is None:
48
+ continue
49
+ if not xml_utils.event_overlaps_window(start_ns, start_ns + duration_ns, window):
50
+ continue
51
+
52
+ process_el = row.get("process")
53
+ process = (
54
+ xml_utils.extract_process(process_el, stream)
55
+ if process_el is not None else None
56
+ )
57
+
58
+ narrative_el = row.get("narrative-description")
59
+ narrative = xml_utils.str_text(stream.resolve(narrative_el)) if narrative_el is not None else None
60
+ if narrative:
61
+ narrative_counts[narrative] += 1
62
+
63
+ is_system_el = row.get("is-system")
64
+ is_system = _bool_text(stream.resolve(is_system_el)) if is_system_el is not None else None
65
+ if is_system:
66
+ system_count += 1
67
+
68
+ events.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 time
73
+ "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
+ })
81
+
82
+ events.sort(key=lambda e: e["duration_ns"], reverse=True)
83
+
84
+ total_hitch_ms = sum(e["hitch_duration_ms"] for e in events)
85
+ worst = events[0] if events else None
86
+
87
+ per_process: dict[str, int] = {}
88
+ for e in events:
89
+ key = e["process"] or "unknown"
90
+ per_process[key] = per_process.get(key, 0) + 1
91
+
92
+ top_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
+ }
101
+ for e in events[:top_n]
102
+ ]
103
+
104
+ return {
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
+ }
121
+
122
+
123
+ def _pick_schema(available: frozenset[str]) -> str | None:
124
+ for s in CANDIDATE_SCHEMAS:
125
+ if s in available:
126
+ return s
127
+ return None
128
+
129
+
130
+ def _first_int(row, stream, keys):
131
+ for key in keys:
132
+ el = row.get(key)
133
+ if el is None:
134
+ continue
135
+ val = xml_utils.int_text(stream.resolve(el))
136
+ if val is not None:
137
+ return val
138
+ return None
139
+
140
+
141
+ def _bool_text(elem) -> bool | None:
142
+ txt = xml_utils.str_text(elem)
143
+ if txt is None:
144
+ return None
145
+ return txt.strip() in ("1", "true", "True", "YES", "Yes")
@@ -0,0 +1,243 @@
1
+ """Markdown summary renderer for the combined trace analysis."""
2
+ from __future__ import annotations
3
+
4
+
5
+ def render(result: dict) -> str:
6
+ lines: list[str] = []
7
+ trace = result.get("trace", "?")
8
+ header = result.get("xctrace_version") or ""
9
+ template = result.get("template") or ""
10
+ duration_s = result.get("duration_s")
11
+ lines.append(f"# Instruments Trace Analysis")
12
+ meta = [p for p in [f"Trace: `{trace}`", header, template] if p]
13
+ lines.append(" • ".join(meta))
14
+ if duration_s is not None:
15
+ lines.append(f"Recording duration: {duration_s:.2f}s")
16
+ lines.append("")
17
+
18
+ lanes_by_name = {lane["lane"]: lane for lane in result.get("lanes", [])}
19
+
20
+ _render_time_profiler(lines, lanes_by_name.get("time-profiler"))
21
+ _render_hangs(lines, lanes_by_name.get("hangs"))
22
+ _render_hitches(lines, lanes_by_name.get("hitches"))
23
+ _render_swiftui(lines, lanes_by_name.get("swiftui"))
24
+ _render_causes(lines, lanes_by_name.get("swiftui-causes"))
25
+ _render_correlations(lines, result.get("correlations", []))
26
+
27
+ return "\n".join(lines).rstrip() + "\n"
28
+
29
+
30
+ def _skipped_block(title: str, lane: dict | None) -> list[str]:
31
+ if lane is None:
32
+ return [f"## {title} — skipped (lane module not run)", ""]
33
+ notes = lane.get("notes") or []
34
+ note_text = f" — {notes[0]}" if notes else ""
35
+ return [f"## {title} — skipped{note_text}", ""]
36
+
37
+
38
+ def _render_time_profiler(lines: list[str], lane: dict | None) -> None:
39
+ if not lane or not lane.get("available"):
40
+ lines.extend(_skipped_block("Time Profiler", lane))
41
+ return
42
+ m = lane["metrics"]
43
+ lines.append(
44
+ f"## Time Profiler — {m['total_samples']:,} samples, "
45
+ f"{m['total_weight_ms']:.0f}ms CPU time"
46
+ )
47
+ if m.get("processes"):
48
+ lines.append(f"Processes: {', '.join(m['processes'])}")
49
+ lines.append("")
50
+ if lane["top_offenders"]:
51
+ lines.append("Top offenders:")
52
+ for i, o in enumerate(lane["top_offenders"], 1):
53
+ lines.append(
54
+ f"{i}. `{_truncate(o['symbol'], 90)}` — "
55
+ f"{o['percent']:.1f}% ({o['weight_ms']:.0f}ms, "
56
+ f"{o['samples']} samples, {_short_thread(o['thread'])})"
57
+ )
58
+ for note in lane.get("notes") or []:
59
+ lines.append(f"> {note}")
60
+ lines.append("")
61
+
62
+
63
+ def _render_hangs(lines: list[str], lane: dict | None) -> None:
64
+ if not lane or not lane.get("available"):
65
+ lines.extend(_skipped_block("Hangs", lane))
66
+ return
67
+ m = lane["metrics"]
68
+ buckets = m["severity_buckets"]
69
+ lines.append(
70
+ f"## Hangs — {m['count']} hangs, {m['total_duration_ms']:.0f}ms total, "
71
+ f"worst {m['worst_duration_ms']:.0f}ms"
72
+ )
73
+ lines.append(
74
+ f"Severity: <250ms={buckets['lt_250ms']}, "
75
+ f"250ms–1s={buckets['250ms_1s']}, >1s={buckets['gt_1s']}"
76
+ )
77
+ lines.append("")
78
+ for i, h in enumerate(lane["top_offenders"], 1):
79
+ lines.append(
80
+ f"{i}. {h['duration_ms']:.0f}ms {h['hang_type']} at "
81
+ f"{h['start_ms']:.2f}ms on {_short_thread(h['thread'])}"
82
+ )
83
+ lines.append("")
84
+
85
+
86
+ def _render_hitches(lines: list[str], lane: dict | None) -> None:
87
+ if not lane or not lane.get("available"):
88
+ lines.extend(_skipped_block("Animation Hitches", lane))
89
+ return
90
+ m = lane["metrics"]
91
+ lines.append(
92
+ f"## Animation Hitches — {m['count']} hitches, "
93
+ f"{m['total_hitch_ms']:.0f}ms total, worst {m['worst_hitch_ms']:.0f}ms"
94
+ )
95
+ if m.get("per_process"):
96
+ pp = ", ".join(f"{k}={v}" for k, v in m["per_process"].items())
97
+ lines.append(f"By process: {pp}")
98
+ lines.append("")
99
+ if m.get("narrative_breakdown"):
100
+ nb = ", ".join(f"{k}={v}" for k, v in m["narrative_breakdown"].items() if k)
101
+ if nb:
102
+ lines.append(f"Apple attribution: {nb}")
103
+ if m.get("system_hitches") is not None:
104
+ lines.append(
105
+ f"System vs app: system={m['system_hitches']}, app={m['app_hitches']}"
106
+ )
107
+ lines.append("")
108
+ for i, h in enumerate(lane["top_offenders"], 1):
109
+ narrative = f" — {h['narrative']}" if h.get("narrative") else ""
110
+ src = " [system]" if h.get("is_system") else ""
111
+ proc = f" ({h['process']})" if h.get("process") else ""
112
+ lines.append(
113
+ f"{i}. {h['hitch_duration_ms']:.0f}ms at {h['start_ms']:.2f}ms"
114
+ f"{proc}{src}{narrative}"
115
+ )
116
+ lines.append("")
117
+
118
+
119
+ def _render_swiftui(lines: list[str], lane: dict | None) -> None:
120
+ if not lane or not lane.get("available"):
121
+ lines.extend(_skipped_block("SwiftUI", lane))
122
+ return
123
+ m = lane["metrics"]
124
+ lines.append(
125
+ f"## SwiftUI — {m['total_events']:,} updates across "
126
+ f"{m['unique_views']} views, {m['total_duration_ms']:.0f}ms total"
127
+ )
128
+ if m.get("severity_breakdown"):
129
+ sb = ", ".join(f"{k}={v}" for k, v in m["severity_breakdown"].items())
130
+ lines.append(f"Severity: {sb}")
131
+ if m.get("update_type_breakdown"):
132
+ ub = ", ".join(f"{k}={v}" for k, v in m["update_type_breakdown"].items())
133
+ lines.append(f"Update types: {ub}")
134
+ lines.append("")
135
+ if lane["top_offenders"]:
136
+ lines.append("Heaviest views (by total body time):")
137
+ for i, v in enumerate(lane["top_offenders"], 1):
138
+ lines.append(
139
+ f"{i}. `{_truncate(v['view'], 80)}` — {v['total_ms']:.0f}ms total, "
140
+ f"{v['count']} updates (avg {v['avg_ms']:.2f}ms)"
141
+ )
142
+ if lane.get("high_severity_events"):
143
+ lines.append("")
144
+ lines.append("High-severity updates:")
145
+ for i, e in enumerate(lane["high_severity_events"][:5], 1):
146
+ cat = f" [{e['category']}]" if e.get("category") else ""
147
+ lines.append(
148
+ f"{i}. `{_truncate(e['view'], 60)}` — "
149
+ f"{e['severity']} ({e['duration_ms']:.2f}ms at {e['start_ms']:.2f}ms){cat}"
150
+ )
151
+ lines.append("")
152
+
153
+
154
+ def _render_causes(lines: list[str], lane: dict | None) -> None:
155
+ if not lane or not lane.get("available"):
156
+ lines.extend(_skipped_block("SwiftUI Cause Graph", lane))
157
+ return
158
+ m = lane["metrics"]
159
+ lines.append(
160
+ f"## SwiftUI Cause Graph — {m['total_edges']:,} edges, "
161
+ f"{m['unique_sources']} sources → {m['unique_destinations']} destinations"
162
+ )
163
+ lines.append("")
164
+ if lane.get("top_sources"):
165
+ lines.append("Top sources (who's driving the most updates):")
166
+ for i, s in enumerate(lane["top_sources"][:5], 1):
167
+ lines.append(f"{i}. `{_truncate(s['source'], 80)}` — {s['edges']:,} edges")
168
+ for d in s["top_destinations"][:3]:
169
+ lines.append(
170
+ f" → `{_truncate(d['destination'], 70)}` {d['edges']:,}"
171
+ )
172
+ if lane.get("top_destinations"):
173
+ lines.append("")
174
+ lines.append("Top destinations (who's being invalidated most):")
175
+ for i, d in enumerate(lane["top_destinations"][:5], 1):
176
+ lines.append(f"{i}. `{_truncate(d['destination'], 80)}` — {d['edges']:,} edges")
177
+ for s in d["top_sources"][:3]:
178
+ lines.append(
179
+ f" ← `{_truncate(s['source'], 70)}` {s['edges']:,}"
180
+ )
181
+ lines.append("")
182
+
183
+
184
+ def _render_correlations(lines: list[str], correlations: list[dict]) -> None:
185
+ if not correlations:
186
+ return
187
+ lines.append("## Correlations")
188
+ lines.append("")
189
+ for c in correlations:
190
+ t = c["trigger"]
191
+ head = (
192
+ f"- **{t['lane']}** at {t['start_ms']:.2f}ms "
193
+ f"({t['duration_ms']:.0f}ms)"
194
+ )
195
+ if t.get("hang_type"):
196
+ head += f" — {t['hang_type']}"
197
+ lines.append(head)
198
+
199
+ tp = c.get("time_profiler_main_thread")
200
+ if tp is not None:
201
+ cov = tp["main_running_coverage_pct"]
202
+ lines.append(
203
+ f" - Main thread: {tp['samples_on_main']} running samples "
204
+ f"({cov:.0f}% coverage — "
205
+ f"{'blocked' if cov < 25 else 'mostly running'})"
206
+ )
207
+ for s in tp["hot_symbols"][:3]:
208
+ lines.append(
209
+ f" · `{_truncate(s['symbol'], 80)}` "
210
+ f"{s['percent_of_main']:.0f}% ({s['samples']} samples)"
211
+ )
212
+ if not tp["hot_symbols"]:
213
+ lines.append(" · no main-thread samples in window")
214
+
215
+ sui = c.get("swiftui_overlapping_updates")
216
+ if sui:
217
+ for s in sui[:3]:
218
+ lines.append(
219
+ f" - SwiftUI: `{s['view']}` {s['duration_ms']:.2f}ms "
220
+ f"(at {s['start_ms']:.2f}ms)"
221
+ )
222
+ lines.append("")
223
+
224
+
225
+ def _short_thread(name: str) -> str:
226
+ if not name:
227
+ return ""
228
+ if name.startswith("Main Thread") or name == "main":
229
+ return "main"
230
+ # "NowPlaying Gigs (0x251990d) (NowPlaying Gigs, pid: 28401)" -> "tid 0x251990d"
231
+ tid_start = name.find("(0x")
232
+ if tid_start != -1:
233
+ start = tid_start + 1
234
+ end = name.find(")", start)
235
+ if end != -1:
236
+ return f"tid {name[start:end]}"
237
+ return name[:40]
238
+
239
+
240
+ def _truncate(s: str, n: int) -> str:
241
+ if len(s) <= n:
242
+ return s
243
+ return s[: n - 1] + "…"
@@ -0,0 +1,195 @@
1
+ """SwiftUI lane parser (Xcode 26+).
2
+
3
+ Primary schema is `swiftui-updates` with columns: start, duration, id,
4
+ update-type, allocations, description, category, view-hierarchy, module,
5
+ view-name, process, thread, root-causes, severity, cause-graph-node,
6
+ full-cause-graph-node.
7
+
8
+ We aggregate by view-name across all SwiftUI schemas (future-proofing against
9
+ schema renames) and break severity out separately so the agent can focus on
10
+ the high-severity rows.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from collections import Counter, defaultdict
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from . import xctrace, xml_utils
19
+
20
+ START_KEYS = ("start", "time", "sample-time", "timestamp")
21
+ DURATION_KEYS = ("duration", "body-duration", "update-duration")
22
+ VIEW_KEYS = ("view-name", "view", "view-type", "name", "type")
23
+ MODULE_KEYS = ("module",)
24
+ CATEGORY_KEYS = ("category",)
25
+ UPDATE_TYPE_KEYS = ("update-type",)
26
+ SEVERITY_KEYS = ("severity",)
27
+ DESCRIPTION_KEYS = ("description",)
28
+
29
+ HIGH_SEVERITIES = {"High", "Very High", "Severe", "Critical"}
30
+
31
+ # Ongoing / unterminated updates carry a sentinel duration (≈ UINT64_MAX-ish).
32
+ # Any duration longer than an hour is almost certainly that sentinel and would
33
+ # break aggregates + the correlation overlap check.
34
+ _SENTINEL_DURATION_NS = 60 * 60 * 1_000_000_000 # 1 hour
35
+
36
+
37
+ def analyze(
38
+ trace_path: Path,
39
+ toc_schemas: frozenset[str],
40
+ top_n: int = 10,
41
+ window: tuple[int, int] | None = None,
42
+ run: int = 1,
43
+ ) -> dict[str, Any]:
44
+ schemas = sorted(
45
+ s for s in toc_schemas
46
+ if s.startswith("swiftui") and not s.endswith("-causes")
47
+ )
48
+ if not schemas:
49
+ return {
50
+ "lane": "swiftui",
51
+ "available": False,
52
+ "notes": ["SwiftUI lane not in trace (Xcode 26+ SwiftUI template required)."],
53
+ }
54
+
55
+ events: list[dict] = []
56
+ per_view_total_ns: dict[str, int] = defaultdict(int)
57
+ per_view_count: dict[str, int] = defaultdict(int)
58
+ severity_counts: Counter[str] = Counter()
59
+ update_type_counts: Counter[str] = Counter()
60
+ category_counts: Counter[str] = Counter()
61
+
62
+ for schema in schemas:
63
+ xml_bytes = xctrace.export_schema(trace_path, schema, run=run)
64
+ stream = xml_utils.RowStream(xml_bytes)
65
+ for row in stream:
66
+ start_ns = _first_int(row, stream, START_KEYS)
67
+ dur_ns = _first_int(row, stream, DURATION_KEYS)
68
+ if start_ns is None or dur_ns is None:
69
+ continue
70
+ if dur_ns < 0 or dur_ns > _SENTINEL_DURATION_NS:
71
+ # Unterminated / ongoing update; skip so it doesn't poison
72
+ # totals and the correlation overlap check.
73
+ continue
74
+ if not xml_utils.event_overlaps_window(start_ns, start_ns + dur_ns, window):
75
+ continue
76
+
77
+ view = _first_str(row, stream, VIEW_KEYS)
78
+ module = _first_str(row, stream, MODULE_KEYS)
79
+ category = _first_str(row, stream, CATEGORY_KEYS)
80
+ update_type = _first_str(row, stream, UPDATE_TYPE_KEYS)
81
+ severity = _first_str(row, stream, SEVERITY_KEYS)
82
+ description = _first_str(row, stream, DESCRIPTION_KEYS)
83
+ # Fall back through description → category → update-type so the
84
+ # agent sees "EnvironmentWriter: RootEnvironment" instead of
85
+ # "<unknown>" when SwiftUI doesn't record a view type.
86
+ if not view:
87
+ view = description or category or update_type or "<unknown>"
88
+
89
+ per_view_total_ns[view] += dur_ns
90
+ per_view_count[view] += 1
91
+ if severity:
92
+ severity_counts[severity] += 1
93
+ if update_type:
94
+ update_type_counts[update_type] += 1
95
+ if category:
96
+ category_counts[category] += 1
97
+
98
+ events.append({
99
+ "schema": schema,
100
+ "start_ns": start_ns,
101
+ "end_ns": start_ns + dur_ns,
102
+ "duration_ns": dur_ns,
103
+ "duration_ms": round(dur_ns / 1_000_000, 2),
104
+ "start_ms": round(start_ns / 1_000_000, 2),
105
+ "view": view,
106
+ "module": module,
107
+ "category": category,
108
+ "update_type": update_type,
109
+ "severity": severity,
110
+ "description": description,
111
+ })
112
+
113
+ events.sort(key=lambda e: e["duration_ns"], reverse=True)
114
+
115
+ top_by_total = sorted(
116
+ per_view_total_ns.items(), key=lambda kv: kv[1], reverse=True
117
+ )[:top_n]
118
+ top_offenders = [
119
+ {
120
+ "view": view,
121
+ "total_ms": round(total_ns / 1_000_000, 2),
122
+ "count": per_view_count[view],
123
+ "avg_ms": round(total_ns / per_view_count[view] / 1_000_000, 2),
124
+ }
125
+ for view, total_ns in top_by_total
126
+ ]
127
+
128
+ high_severity = [
129
+ {
130
+ "view": e["view"],
131
+ "severity": e["severity"],
132
+ "duration_ms": e["duration_ms"],
133
+ "start_ms": e["start_ms"],
134
+ "category": e["category"],
135
+ "update_type": e["update_type"],
136
+ "description": e["description"],
137
+ }
138
+ for e in events if e["severity"] in HIGH_SEVERITIES
139
+ ][:top_n]
140
+
141
+ longest = [
142
+ {
143
+ "view": e["view"],
144
+ "duration_ms": e["duration_ms"],
145
+ "start_ms": e["start_ms"],
146
+ "category": e["category"],
147
+ "update_type": e["update_type"],
148
+ "severity": e["severity"],
149
+ }
150
+ for e in events[:top_n]
151
+ ]
152
+
153
+ return {
154
+ "lane": "swiftui",
155
+ "available": True,
156
+ "schemas_used": schemas,
157
+ "metrics": {
158
+ "total_events": len(events),
159
+ "unique_views": len(per_view_total_ns),
160
+ "total_duration_ms": round(
161
+ sum(per_view_total_ns.values()) / 1_000_000, 2
162
+ ),
163
+ "severity_breakdown": dict(severity_counts.most_common()),
164
+ "update_type_breakdown": dict(update_type_counts.most_common()),
165
+ "category_breakdown": dict(category_counts.most_common()),
166
+ },
167
+ "top_offenders": top_offenders,
168
+ "longest_single_events": longest,
169
+ "high_severity_events": high_severity,
170
+ "notes": [],
171
+ "_events": events,
172
+ }
173
+
174
+
175
+ def _first_int(row, stream, keys):
176
+ for key in keys:
177
+ el = row.get(key)
178
+ if el is None:
179
+ continue
180
+ val = xml_utils.int_text(stream.resolve(el))
181
+ if val is not None:
182
+ return val
183
+ return None
184
+
185
+
186
+ def _first_str(row, stream, keys):
187
+ for key in keys:
188
+ el = row.get(key)
189
+ if el is None:
190
+ continue
191
+ resolved = stream.resolve(el)
192
+ txt = xml_utils.str_text(resolved) or resolved.get("fmt")
193
+ if txt:
194
+ return txt
195
+ return None