claudecode-omc 5.6.6 → 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 (179) 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/h5-to-swiftui/SKILL.md +201 -0
  6. package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
  7. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
  8. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
  9. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
  10. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
  11. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
  12. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
  13. package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
  14. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
  15. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
  16. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
  17. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
  18. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
  19. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
  20. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
  21. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
  22. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
  23. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
  24. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
  25. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
  26. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
  27. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
  28. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
  29. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
  30. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
  31. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
  32. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
  33. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
  34. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
  35. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
  36. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
  37. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
  38. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
  39. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
  40. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
  41. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
  42. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
  43. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
  44. package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
  45. package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
  46. package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
  47. package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
  48. package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
  49. package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
  50. package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
  51. package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
  52. package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
  53. package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
  54. package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
  55. package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
  56. package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
  57. package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
  58. package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
  59. package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
  60. package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
  61. package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
  62. package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
  63. package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
  64. package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
  65. package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
  66. package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
  67. package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
  68. package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
  69. package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
  70. package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  71. package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  72. package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  73. package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  74. package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
  75. package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
  76. package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  77. package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
  78. package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  79. package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
  80. package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
  81. package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  82. package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  83. package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  84. package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  85. package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  86. package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  87. package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
  88. package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
  89. package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
  90. package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
  91. package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
  92. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
  93. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
  94. package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
  95. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
  96. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
  98. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
  99. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
  100. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
  101. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
  102. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
  103. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
  104. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
  105. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
  106. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
  107. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
  108. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
  109. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
  110. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
  111. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
  112. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
  113. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
  114. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
  115. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
  116. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
  117. package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
  118. package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
  119. package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
  120. package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
  121. package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
  122. package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
  123. package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
  124. package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  125. package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  126. package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
  127. package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
  128. package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  129. package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  130. package/.local/skills/swiftui-pro/SKILL.md +108 -0
  131. package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
  132. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
  133. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
  134. package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
  135. package/.local/skills/swiftui-pro/references/api.md +39 -0
  136. package/.local/skills/swiftui-pro/references/data.md +43 -0
  137. package/.local/skills/swiftui-pro/references/design.md +32 -0
  138. package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
  139. package/.local/skills/swiftui-pro/references/navigation.md +14 -0
  140. package/.local/skills/swiftui-pro/references/performance.md +46 -0
  141. package/.local/skills/swiftui-pro/references/swift.md +56 -0
  142. package/.local/skills/swiftui-pro/references/views.md +36 -0
  143. package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
  144. package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
  145. package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  146. package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  147. package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  148. package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
  149. package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  150. package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
  151. package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
  152. package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
  153. package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  154. package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  155. package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  156. package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
  157. package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  158. package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  159. package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  160. package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
  161. package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  162. package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  163. package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  164. package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
  165. package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
  166. package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  167. package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  168. package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  169. package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  170. package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  171. package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  172. package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
  173. package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  174. package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
  175. package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
  176. package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
  177. package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
  178. package/bundled/manifest.json +1 -1
  179. package/package.json +1 -1
@@ -0,0 +1,135 @@
1
+ """Time Profiler lane parser (schema `time-profile`).
2
+
3
+ Aggregates CPU samples by leaf symbol, keeps per-sample rows so that other
4
+ lanes can correlate by timestamp window.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections import defaultdict
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from . import xctrace, xml_utils
13
+
14
+ PREFERRED_SCHEMAS = ("time-profile",)
15
+ FALLBACK_SCHEMAS = ("time-sample",) # no symbolication; used only if nothing else
16
+
17
+
18
+ def analyze(
19
+ trace_path: Path,
20
+ toc_schemas: frozenset[str],
21
+ top_n: int = 10,
22
+ window: tuple[int, int] | None = None,
23
+ run: int = 1,
24
+ ) -> dict[str, Any]:
25
+ schema = _pick_schema(toc_schemas)
26
+ if schema is None:
27
+ return {
28
+ "lane": "time-profiler",
29
+ "available": False,
30
+ "notes": ["Time Profiler data not present in trace."],
31
+ }
32
+
33
+ xml_bytes = xctrace.export_schema(trace_path, schema, run=run)
34
+ stream = xml_utils.RowStream(xml_bytes)
35
+
36
+ samples: list[dict] = []
37
+ symbol_weight: dict[str, int] = defaultdict(int)
38
+ symbol_samples: dict[str, int] = defaultdict(int)
39
+ symbol_thread: dict[str, str] = {}
40
+ processes: set[str] = set()
41
+ total_weight = 0
42
+ min_time: int | None = None
43
+ max_time: int | None = None
44
+
45
+ for row in stream:
46
+ time_el = row.get("time")
47
+ weight_el = row.get("weight")
48
+ thread_el = row.get("thread")
49
+ stack_el = row.get("stack")
50
+ if stack_el is None or time_el is None or thread_el is None:
51
+ continue
52
+
53
+ sample_time_ns = xml_utils.int_text(stream.resolve(time_el))
54
+ if not xml_utils.in_window(sample_time_ns, window):
55
+ continue
56
+ weight_ns = xml_utils.int_text(stream.resolve(weight_el)) or 0
57
+ frames = xml_utils.extract_backtrace(stack_el, stream, max_frames=20)
58
+ if not frames:
59
+ continue
60
+
61
+ thread = xml_utils.extract_thread(thread_el, stream)
62
+ process_name = (thread.get("process") or {}).get("name")
63
+ if process_name:
64
+ processes.add(process_name)
65
+
66
+ leaf = xml_utils.top_symbol(frames)
67
+ symbol_weight[leaf] += weight_ns
68
+ symbol_samples[leaf] += 1
69
+ symbol_thread.setdefault(
70
+ leaf, "main" if thread["is_main"] else thread.get("name", "")
71
+ )
72
+ total_weight += weight_ns
73
+
74
+ if sample_time_ns is not None:
75
+ min_time = sample_time_ns if min_time is None else min(min_time, sample_time_ns)
76
+ max_time = sample_time_ns if max_time is None else max(max_time, sample_time_ns)
77
+
78
+ samples.append({
79
+ "time_ns": sample_time_ns,
80
+ "weight_ns": weight_ns,
81
+ "thread_name": thread["name"],
82
+ "is_main": thread["is_main"],
83
+ "process": process_name,
84
+ "leaf_symbol": leaf,
85
+ "frames": frames[:5],
86
+ })
87
+
88
+ samples.sort(key=lambda s: s["time_ns"])
89
+
90
+ top = sorted(
91
+ symbol_weight.items(), key=lambda kv: kv[1], reverse=True
92
+ )[:top_n]
93
+ top_offenders = [
94
+ {
95
+ "symbol": sym,
96
+ "weight_ns": w,
97
+ "weight_ms": round(w / 1_000_000, 2),
98
+ "samples": symbol_samples[sym],
99
+ "percent": round(100.0 * w / total_weight, 2) if total_weight else 0.0,
100
+ "thread": symbol_thread.get(sym, ""),
101
+ }
102
+ for sym, w in top
103
+ ]
104
+
105
+ notes: list[str] = []
106
+ if schema in FALLBACK_SCHEMAS:
107
+ notes.append(
108
+ f"Using fallback schema `{schema}`; backtraces may be unsymbolicated."
109
+ )
110
+
111
+ return {
112
+ "lane": "time-profiler",
113
+ "available": True,
114
+ "schema_used": schema,
115
+ "metrics": {
116
+ "total_samples": len(samples),
117
+ "total_weight_ns": total_weight,
118
+ "total_weight_ms": round(total_weight / 1_000_000, 2),
119
+ "window_start_ns": min_time,
120
+ "window_end_ns": max_time,
121
+ "processes": sorted(processes),
122
+ },
123
+ "top_offenders": top_offenders,
124
+ "notes": notes,
125
+ # Internal: retained for correlation. Stripped before JSON emission
126
+ # if --slim is requested by the orchestrator.
127
+ "_samples": samples,
128
+ }
129
+
130
+
131
+ def _pick_schema(available: frozenset[str]) -> str | None:
132
+ for s in PREFERRED_SCHEMAS + FALLBACK_SCHEMAS:
133
+ if s in available:
134
+ return s
135
+ return None
@@ -0,0 +1,117 @@
1
+ """Thin wrapper around the `xctrace` CLI."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import xml.etree.ElementTree as ET
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class RunInfo:
12
+ """Per-run metadata and schemas. Instruments traces can hold multiple runs."""
13
+ number: int
14
+ template_name: str | None
15
+ duration_s: float | None
16
+ start_date: str | None
17
+ end_date: str | None
18
+ schemas: frozenset[str]
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TraceInfo:
23
+ xctrace_version: str
24
+ runs: tuple[RunInfo, ...]
25
+
26
+ def get_run(self, number: int) -> RunInfo:
27
+ for r in self.runs:
28
+ if r.number == number:
29
+ return r
30
+ available = ", ".join(str(r.number) for r in self.runs)
31
+ raise KeyError(f"run {number} not in trace (available: {available})")
32
+
33
+
34
+ def version() -> str:
35
+ out = subprocess.run(
36
+ ["xctrace", "version"], capture_output=True, text=True, check=True
37
+ )
38
+ return out.stdout.strip()
39
+
40
+
41
+ def toc(trace_path: Path) -> TraceInfo:
42
+ """Export the trace's table of contents and return per-run metadata.
43
+
44
+ The TOC is small (a few KB) so we load it fully rather than streaming.
45
+ """
46
+ xml_bytes = _run_export(trace_path, ["--toc"])
47
+ root = ET.fromstring(xml_bytes)
48
+
49
+ instruments = _find_text(root, ".//instruments-version") or ""
50
+
51
+ runs: list[RunInfo] = []
52
+ for run_el in root.iterfind("./run"):
53
+ number_attr = run_el.get("number")
54
+ if not number_attr:
55
+ continue
56
+ try:
57
+ number = int(number_attr)
58
+ except ValueError:
59
+ continue
60
+ if number <= 0:
61
+ continue
62
+
63
+ schemas: set[str] = set()
64
+ for table in run_el.iterfind("./data/table"):
65
+ schema = table.get("schema")
66
+ if schema:
67
+ schemas.add(schema)
68
+
69
+ summary = run_el.find("./info/summary")
70
+ if summary is not None:
71
+ template = _find_text(summary, "./template-name")
72
+ duration = _find_text(summary, "./duration")
73
+ start = _find_text(summary, "./start-date")
74
+ end = _find_text(summary, "./end-date")
75
+ else:
76
+ template = duration = start = end = None
77
+
78
+ runs.append(RunInfo(
79
+ number=number,
80
+ template_name=template,
81
+ duration_s=float(duration) if duration else None,
82
+ start_date=start,
83
+ end_date=end,
84
+ schemas=frozenset(schemas),
85
+ ))
86
+
87
+ runs.sort(key=lambda r: r.number)
88
+ return TraceInfo(
89
+ xctrace_version=instruments,
90
+ runs=tuple(runs),
91
+ )
92
+
93
+
94
+ def export_schema(trace_path: Path, schema: str, run: int = 1) -> bytes:
95
+ """Export one schema's data as XML bytes from the given run.
96
+
97
+ Callers are expected to iterparse the result rather than build a full tree
98
+ for large schemas (time-profile can be tens of MB).
99
+ """
100
+ xpath = f'/trace-toc/run[@number="{run}"]/data/table[@schema="{schema}"]'
101
+ return _run_export(trace_path, ["--xpath", xpath])
102
+
103
+
104
+ def _run_export(trace_path: Path, extra_args: list[str]) -> bytes:
105
+ cmd = ["xctrace", "export", "--input", str(trace_path), *extra_args]
106
+ proc = subprocess.run(cmd, capture_output=True, check=False)
107
+ if proc.returncode != 0:
108
+ raise RuntimeError(
109
+ f"xctrace export failed ({proc.returncode}): "
110
+ f"{proc.stderr.decode(errors='replace').strip()}"
111
+ )
112
+ return proc.stdout
113
+
114
+
115
+ def _find_text(root: ET.Element, path: str) -> str | None:
116
+ el = root.find(path)
117
+ return el.text if el is not None and el.text else None
@@ -0,0 +1,224 @@
1
+ """Streaming XML helpers for xctrace export output.
2
+
3
+ Instruments XML deduplicates repeated values with `id`/`ref` attributes that
4
+ can span the whole document, so we stream rows with iterparse while keeping
5
+ a global id cache for later ref lookups.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import xml.etree.ElementTree as ET
10
+ from collections.abc import Iterator
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Column:
16
+ mnemonic: str # e.g. "time", "weight", "stack"
17
+ engineering_type: str # e.g. "sample-time", "weight", "tagged-backtrace"
18
+
19
+
20
+ class RowStream:
21
+ """Iterate <row> elements of a single <table> schema export.
22
+
23
+ Yields `dict[str, Element]` keyed by column mnemonic. Elements inside a
24
+ yielded row are live ET elements (rooted in the id cache where applicable
25
+ so ref resolution via `resolve()` remains valid after the row is yielded).
26
+ """
27
+
28
+ def __init__(self, xml_bytes: bytes):
29
+ self._xml = xml_bytes
30
+ self.columns: list[Column] = []
31
+ self._id_cache: dict[str, ET.Element] = {}
32
+
33
+ def resolve(self, element: ET.Element) -> ET.Element:
34
+ """If the element is a ref, return the referenced element; else self."""
35
+ ref = element.get("ref")
36
+ if ref is None:
37
+ return element
38
+ target = self._id_cache.get(ref)
39
+ if target is None:
40
+ return element # unresolved; return the ref element itself
41
+ return target
42
+
43
+ def __iter__(self) -> Iterator[dict[str, ET.Element]]:
44
+ # iterparse fires `end` events once an element is fully parsed, so ids
45
+ # are visible to descendants via the cache. We only need `end` events;
46
+ # row bodies are reconstructed from the end element itself in _row_dict.
47
+ #
48
+ # NOTE: we intentionally don't call `elem.clear()` after yielding a row.
49
+ # Instruments' XML is a single shared doc where any row can `ref` an
50
+ # `id` defined earlier (threads, processes, stacks, metadata), and
51
+ # clearing would break those later lookups. The tradeoff is peak RAM
52
+ # ≈ document size. That's fine for typical traces up to a few hundred
53
+ # MB; very large exports may need a smarter pass that first indexes
54
+ # referenced ids and only retains those.
55
+ schema_seen = False
56
+
57
+ context = ET.iterparse(_bytes_to_file(self._xml), events=("end",))
58
+ for _event, elem in context:
59
+ eid = elem.get("id")
60
+ if eid is not None:
61
+ self._id_cache[eid] = elem
62
+
63
+ if elem.tag == "schema" and not schema_seen:
64
+ self.columns = _parse_columns(elem)
65
+ schema_seen = True
66
+ continue
67
+
68
+ if elem.tag == "row":
69
+ yield _row_dict(elem, self.columns)
70
+ # Do not clear elem — children referenced via id may still be needed.
71
+
72
+
73
+ def _parse_columns(schema_el: ET.Element) -> list[Column]:
74
+ cols: list[Column] = []
75
+ for col in schema_el.findall("col"):
76
+ mnemonic = (col.findtext("mnemonic") or "").strip()
77
+ etype = (col.findtext("engineering-type") or "").strip()
78
+ if mnemonic:
79
+ cols.append(Column(mnemonic=mnemonic, engineering_type=etype))
80
+ return cols
81
+
82
+
83
+ def _row_dict(row_el: ET.Element, cols: list[Column]) -> dict[str, ET.Element]:
84
+ # Row children map positionally to columns. <sentinel/> marks a missing
85
+ # optional value for that column.
86
+ result: dict[str, ET.Element] = {}
87
+ children = list(row_el)
88
+ for idx, child in enumerate(children):
89
+ if idx >= len(cols):
90
+ break
91
+ if child.tag == "sentinel":
92
+ continue
93
+ result[cols[idx].mnemonic] = child
94
+ return result
95
+
96
+
97
+ def _bytes_to_file(data: bytes):
98
+ import io
99
+ return io.BytesIO(data)
100
+
101
+
102
+ # --- Extraction helpers ---------------------------------------------------
103
+
104
+ def int_text(elem: ET.Element | None) -> int | None:
105
+ if elem is None or elem.text is None:
106
+ return None
107
+ try:
108
+ return int(elem.text)
109
+ except ValueError:
110
+ return None
111
+
112
+
113
+ def str_text(elem: ET.Element | None) -> str | None:
114
+ if elem is None or elem.text is None:
115
+ return None
116
+ return elem.text
117
+
118
+
119
+ def fmt_attr(elem: ET.Element | None) -> str | None:
120
+ """Return the human-readable `fmt` attribute if present."""
121
+ if elem is None:
122
+ return None
123
+ return elem.get("fmt")
124
+
125
+
126
+ def extract_thread(thread_el: ET.Element, stream: RowStream) -> dict:
127
+ """Parse a <thread> element into name, tid, process dict.
128
+
129
+ Handles ref-style threads by resolving through the stream's id cache.
130
+ """
131
+ resolved = stream.resolve(thread_el)
132
+ name = resolved.get("fmt", "")
133
+ tid_el = resolved.find("tid")
134
+ process_el = resolved.find("process")
135
+ process = extract_process(process_el, stream) if process_el is not None else None
136
+ return {
137
+ "name": name,
138
+ "tid": int_text(tid_el),
139
+ "process": process,
140
+ "is_main": name.startswith("Main Thread") if name else False,
141
+ }
142
+
143
+
144
+ def extract_process(process_el: ET.Element, stream: RowStream) -> dict:
145
+ resolved = stream.resolve(process_el)
146
+ name = resolved.get("fmt", "")
147
+ pid_el = resolved.find("pid")
148
+ return {
149
+ "name": _clean_process_name(name),
150
+ "pid": int_text(pid_el),
151
+ }
152
+
153
+
154
+ def _clean_process_name(fmt: str) -> str:
155
+ # "NowPlaying Gigs (28401)" -> "NowPlaying Gigs"
156
+ if " (" in fmt and fmt.endswith(")"):
157
+ return fmt.rsplit(" (", 1)[0]
158
+ return fmt
159
+
160
+
161
+ def extract_backtrace(
162
+ bt_el: ET.Element, stream: RowStream, max_frames: int = 20
163
+ ) -> list[dict]:
164
+ """Return a list of frame dicts from a <tagged-backtrace> or <backtrace>.
165
+
166
+ Frames are ordered leaf-first (top of stack first), matching Instruments'
167
+ display order.
168
+ """
169
+ resolved = stream.resolve(bt_el)
170
+ inner = resolved.find("backtrace")
171
+ if inner is None:
172
+ inner = resolved
173
+ frames: list[dict] = []
174
+ for frame_el in inner.findall("frame"):
175
+ f = stream.resolve(frame_el)
176
+ frames.append({
177
+ "name": f.get("name") or "",
178
+ "addr": f.get("addr") or "",
179
+ })
180
+ if len(frames) >= max_frames:
181
+ break
182
+ return frames
183
+
184
+
185
+ def top_symbol(frames: list[dict]) -> str:
186
+ """Pick the leaf symbol, falling back to addr if unsymbolicated."""
187
+ if not frames:
188
+ return "<empty-stack>"
189
+ first = frames[0]
190
+ return first.get("name") or first.get("addr") or "<unknown>"
191
+
192
+
193
+ def first_present(row: dict, *keys: str) -> ET.Element | None:
194
+ """Return the first row column whose key exists.
195
+
196
+ `row[key] or row[other_key]` is unsafe here: Element is falsy when it has
197
+ no children (a common case for leaf <event-time>, <start-time>, etc.), so
198
+ `or` short-circuits past valid leaf elements. This walks keys explicitly.
199
+ """
200
+ for key in keys:
201
+ el = row.get(key)
202
+ if el is not None:
203
+ return el
204
+ return None
205
+
206
+
207
+ def in_window(time_ns: int | None, window: tuple[int, int] | None) -> bool:
208
+ """Return True if time_ns is inside [start, end] (inclusive), or window is None."""
209
+ if window is None:
210
+ return True
211
+ if time_ns is None:
212
+ return False
213
+ start, end = window
214
+ return start <= time_ns <= end
215
+
216
+
217
+ def event_overlaps_window(
218
+ start_ns: int, end_ns: int, window: tuple[int, int] | None
219
+ ) -> bool:
220
+ """Return True if [start, end] overlaps [window.start, window.end]."""
221
+ if window is None:
222
+ return True
223
+ w_start, w_end = window
224
+ return not (end_ns < w_start or start_ns > w_end)