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.
- package/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
- package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
- package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
- package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
- package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
- package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
- package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
- package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
- package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
- package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
- package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
- package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
- package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
- package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
- package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
- package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
- package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
- package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
- package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
- package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
- package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
- package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
- package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
- package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
- package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
- package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
- package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
- package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
- package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
- package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
- package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
- package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
- package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
- package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
- package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
- package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
- package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
- package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
- package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
- package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
- package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
- package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
- package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
- package/.local/skills/swiftui-pro/SKILL.md +108 -0
- package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
- package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
- package/.local/skills/swiftui-pro/references/api.md +39 -0
- package/.local/skills/swiftui-pro/references/data.md +43 -0
- package/.local/skills/swiftui-pro/references/design.md +32 -0
- package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
- package/.local/skills/swiftui-pro/references/navigation.md +14 -0
- package/.local/skills/swiftui-pro/references/performance.md +46 -0
- package/.local/skills/swiftui-pro/references/swift.md +56 -0
- package/.local/skills/swiftui-pro/references/views.md +36 -0
- package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
- package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
- package/bundled/manifest.json +1 -1
- 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)
|