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.
- 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/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
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Analyze an Xcode Instruments .trace file and emit JSON + markdown.
|
|
3
|
+
|
|
4
|
+
Primary modes:
|
|
5
|
+
(default) Full four-lane analysis + cross-lane correlations.
|
|
6
|
+
--list-logs Dump os_log entries (optionally filtered) as JSON so an
|
|
7
|
+
agent can locate a focus window by log content.
|
|
8
|
+
--list-signposts Dump os_signpost intervals + point events as JSON.
|
|
9
|
+
|
|
10
|
+
Windowing:
|
|
11
|
+
--window START_MS:END_MS restricts every lane to that slice of the trace.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from instruments_parser import (
|
|
21
|
+
causes,
|
|
22
|
+
correlate,
|
|
23
|
+
events,
|
|
24
|
+
hangs,
|
|
25
|
+
hitches,
|
|
26
|
+
summary,
|
|
27
|
+
swiftui,
|
|
28
|
+
time_profiler,
|
|
29
|
+
xctrace,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(argv: list[str] | None = None) -> int:
|
|
34
|
+
parser = argparse.ArgumentParser(
|
|
35
|
+
description="Analyze an Instruments .trace file.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument("--trace", required=True, type=Path)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--output",
|
|
40
|
+
type=Path,
|
|
41
|
+
help="Base path; writes <output>.json and <output>.md",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument("--top", type=int, default=10, help="Top-N per lane")
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--top-hitches",
|
|
46
|
+
type=int,
|
|
47
|
+
default=5,
|
|
48
|
+
help="Correlate only the N worst hitches (avoid flooding output).",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--window",
|
|
52
|
+
type=str,
|
|
53
|
+
default=None,
|
|
54
|
+
help="Restrict analysis to a time slice, e.g. --window 10400:11700 (ms).",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--run",
|
|
58
|
+
type=int,
|
|
59
|
+
default=None,
|
|
60
|
+
help="Which run to analyze (1-based). Required for traces with >1 run.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--list-runs", action="store_true",
|
|
64
|
+
help="Emit per-run metadata as JSON (use this to discover available runs).",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Mode flags (mutually exclusive with full analysis)
|
|
68
|
+
mode_group = parser.add_argument_group("Discovery modes")
|
|
69
|
+
mode_group.add_argument(
|
|
70
|
+
"--list-logs", action="store_true",
|
|
71
|
+
help="Emit os_log entries as JSON (use filter flags below).",
|
|
72
|
+
)
|
|
73
|
+
mode_group.add_argument(
|
|
74
|
+
"--list-signposts", action="store_true",
|
|
75
|
+
help="Emit os_signpost intervals + events as JSON.",
|
|
76
|
+
)
|
|
77
|
+
mode_group.add_argument("--log-subsystem", type=str, default=None)
|
|
78
|
+
mode_group.add_argument("--log-category", type=str, default=None)
|
|
79
|
+
mode_group.add_argument(
|
|
80
|
+
"--log-type", type=str, default=None,
|
|
81
|
+
help="e.g. Fault, Error, Default, Info, Debug",
|
|
82
|
+
)
|
|
83
|
+
mode_group.add_argument(
|
|
84
|
+
"--log-message-contains", type=str, default=None,
|
|
85
|
+
help="Case-insensitive substring match on the message / format string.",
|
|
86
|
+
)
|
|
87
|
+
mode_group.add_argument(
|
|
88
|
+
"--log-limit", type=int, default=None,
|
|
89
|
+
help="Cap number of log entries returned (applied after all filters).",
|
|
90
|
+
)
|
|
91
|
+
mode_group.add_argument(
|
|
92
|
+
"--signpost-name-contains", type=str, default=None,
|
|
93
|
+
help="Case-insensitive substring match on signpost name.",
|
|
94
|
+
)
|
|
95
|
+
mode_group.add_argument("--signpost-subsystem", type=str, default=None)
|
|
96
|
+
mode_group.add_argument("--signpost-category", type=str, default=None)
|
|
97
|
+
mode_group.add_argument(
|
|
98
|
+
"--fanin-for", type=str, default=None,
|
|
99
|
+
help="Emit incoming cause-graph sources for destinations whose fmt "
|
|
100
|
+
"contains this substring. Case-insensitive.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
fmt_group = parser.add_mutually_exclusive_group()
|
|
104
|
+
fmt_group.add_argument("--json-only", action="store_true")
|
|
105
|
+
fmt_group.add_argument("--markdown-only", action="store_true")
|
|
106
|
+
|
|
107
|
+
args = parser.parse_args(argv)
|
|
108
|
+
|
|
109
|
+
# The discovery modes aren't in a mutually_exclusive_group because they
|
|
110
|
+
# live alongside their sub-filters in the same argparse group; enforce the
|
|
111
|
+
# constraint by hand so an agent gets a clear error instead of silent
|
|
112
|
+
# precedence.
|
|
113
|
+
active_modes = sum([
|
|
114
|
+
args.list_runs,
|
|
115
|
+
args.list_logs,
|
|
116
|
+
args.list_signposts,
|
|
117
|
+
bool(args.fanin_for),
|
|
118
|
+
])
|
|
119
|
+
if active_modes > 1:
|
|
120
|
+
parser.error(
|
|
121
|
+
"--list-runs, --list-logs, --list-signposts, and --fanin-for are "
|
|
122
|
+
"mutually exclusive; pick one per invocation."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
trace = args.trace
|
|
126
|
+
if not trace.exists():
|
|
127
|
+
print(f"error: trace not found: {trace}", file=sys.stderr)
|
|
128
|
+
return 2
|
|
129
|
+
|
|
130
|
+
info = xctrace.toc(trace)
|
|
131
|
+
window_ns = _parse_window(args.window)
|
|
132
|
+
|
|
133
|
+
if args.list_runs:
|
|
134
|
+
sys.stdout.write(json.dumps({
|
|
135
|
+
"xctrace_version": info.xctrace_version,
|
|
136
|
+
"runs": [
|
|
137
|
+
{
|
|
138
|
+
"number": r.number,
|
|
139
|
+
"template": r.template_name,
|
|
140
|
+
"duration_s": r.duration_s,
|
|
141
|
+
"start_date": r.start_date,
|
|
142
|
+
"end_date": r.end_date,
|
|
143
|
+
"schemas": sorted(r.schemas),
|
|
144
|
+
}
|
|
145
|
+
for r in info.runs
|
|
146
|
+
],
|
|
147
|
+
}, indent=2))
|
|
148
|
+
sys.stdout.write("\n")
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
run_info = _resolve_run(info, args.run)
|
|
152
|
+
if run_info is None:
|
|
153
|
+
return 2
|
|
154
|
+
run_number = run_info.number
|
|
155
|
+
|
|
156
|
+
if args.list_logs:
|
|
157
|
+
out = events.list_logs(
|
|
158
|
+
trace, run_info.schemas,
|
|
159
|
+
subsystem=args.log_subsystem,
|
|
160
|
+
category=args.log_category,
|
|
161
|
+
message_contains=args.log_message_contains,
|
|
162
|
+
message_type=args.log_type,
|
|
163
|
+
limit=args.log_limit,
|
|
164
|
+
window_ns=window_ns,
|
|
165
|
+
run=run_number,
|
|
166
|
+
)
|
|
167
|
+
sys.stdout.write(json.dumps({"logs": out, "count": len(out)}, indent=2))
|
|
168
|
+
sys.stdout.write("\n")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
if args.list_signposts:
|
|
172
|
+
sp = events.list_signposts(
|
|
173
|
+
trace, run_info.schemas,
|
|
174
|
+
name_contains=args.signpost_name_contains,
|
|
175
|
+
subsystem=args.signpost_subsystem,
|
|
176
|
+
category=args.signpost_category,
|
|
177
|
+
window_ns=window_ns,
|
|
178
|
+
run=run_number,
|
|
179
|
+
)
|
|
180
|
+
sys.stdout.write(json.dumps(sp, indent=2))
|
|
181
|
+
sys.stdout.write("\n")
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
if args.fanin_for:
|
|
185
|
+
fanin = causes.fanin_for(
|
|
186
|
+
trace, run_info.schemas,
|
|
187
|
+
destination_contains=args.fanin_for,
|
|
188
|
+
top_k=args.top,
|
|
189
|
+
window=window_ns,
|
|
190
|
+
run=run_number,
|
|
191
|
+
)
|
|
192
|
+
sys.stdout.write(json.dumps(fanin, indent=2))
|
|
193
|
+
sys.stdout.write("\n")
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
# Full five-lane analysis
|
|
197
|
+
schemas = run_info.schemas
|
|
198
|
+
lanes_out = {
|
|
199
|
+
"time-profiler": time_profiler.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
|
|
200
|
+
"hangs": hangs.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
|
|
201
|
+
"hitches": hitches.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
|
|
202
|
+
"swiftui": swiftui.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
|
|
203
|
+
"swiftui-causes": causes.analyze(trace, schemas, top_n=args.top, window=window_ns, run=run_number),
|
|
204
|
+
}
|
|
205
|
+
correlations = correlate.build(
|
|
206
|
+
lanes_out, top_hitches=args.top_hitches, top_symbols=5
|
|
207
|
+
)
|
|
208
|
+
public_lanes = [_strip_internal(l) for l in lanes_out.values()]
|
|
209
|
+
|
|
210
|
+
result: dict = {
|
|
211
|
+
"trace": str(trace),
|
|
212
|
+
"xctrace_version": info.xctrace_version,
|
|
213
|
+
"run": run_number,
|
|
214
|
+
"runs_available": [r.number for r in info.runs],
|
|
215
|
+
"template": run_info.template_name,
|
|
216
|
+
"duration_s": run_info.duration_s,
|
|
217
|
+
"start_date": run_info.start_date,
|
|
218
|
+
"end_date": run_info.end_date,
|
|
219
|
+
"schemas_available": sorted(run_info.schemas),
|
|
220
|
+
"lanes": public_lanes,
|
|
221
|
+
"correlations": correlations,
|
|
222
|
+
}
|
|
223
|
+
if window_ns is not None:
|
|
224
|
+
result["window_ms"] = {
|
|
225
|
+
"start": window_ns[0] / 1_000_000,
|
|
226
|
+
"end": window_ns[1] / 1_000_000,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
md = summary.render(result)
|
|
230
|
+
|
|
231
|
+
if args.output:
|
|
232
|
+
json_path = args.output.with_suffix(".json")
|
|
233
|
+
md_path = args.output.with_suffix(".md")
|
|
234
|
+
json_path.write_text(json.dumps(result, indent=2))
|
|
235
|
+
md_path.write_text(md)
|
|
236
|
+
print(f"wrote {json_path}")
|
|
237
|
+
print(f"wrote {md_path}")
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
if args.markdown_only:
|
|
241
|
+
sys.stdout.write(md)
|
|
242
|
+
elif args.json_only:
|
|
243
|
+
sys.stdout.write(json.dumps(result, indent=2))
|
|
244
|
+
sys.stdout.write("\n")
|
|
245
|
+
else:
|
|
246
|
+
sys.stdout.write(json.dumps(result, indent=2))
|
|
247
|
+
sys.stdout.write("\n---\n")
|
|
248
|
+
sys.stdout.write(md)
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _resolve_run(info, requested: int | None):
|
|
253
|
+
"""Pick a run from the trace.
|
|
254
|
+
|
|
255
|
+
If `requested` is given, return that run or None on miss (with a friendly
|
|
256
|
+
error). If unset and the trace has exactly one run, default to it. If
|
|
257
|
+
unset and there are multiple runs, error out so the agent picks
|
|
258
|
+
explicitly — silently picking run 1 lost data for the user.
|
|
259
|
+
"""
|
|
260
|
+
if not info.runs:
|
|
261
|
+
print("error: trace has no runs", file=sys.stderr)
|
|
262
|
+
return None
|
|
263
|
+
if requested is not None:
|
|
264
|
+
try:
|
|
265
|
+
return info.get_run(requested)
|
|
266
|
+
except KeyError as e:
|
|
267
|
+
print(f"error: {e}", file=sys.stderr)
|
|
268
|
+
return None
|
|
269
|
+
if len(info.runs) == 1:
|
|
270
|
+
return info.runs[0]
|
|
271
|
+
available = ", ".join(str(r.number) for r in info.runs)
|
|
272
|
+
print(
|
|
273
|
+
f"error: trace has {len(info.runs)} runs ({available}); pass --run N. "
|
|
274
|
+
f"Use --list-runs to see per-run metadata.",
|
|
275
|
+
file=sys.stderr,
|
|
276
|
+
)
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _parse_window(spec: str | None) -> tuple[int, int] | None:
|
|
281
|
+
if not spec:
|
|
282
|
+
return None
|
|
283
|
+
if ":" not in spec:
|
|
284
|
+
raise SystemExit(f"--window expects START_MS:END_MS, got {spec!r}")
|
|
285
|
+
start_s, end_s = spec.split(":", 1)
|
|
286
|
+
try:
|
|
287
|
+
start_ms = float(start_s)
|
|
288
|
+
end_ms = float(end_s)
|
|
289
|
+
except ValueError as e:
|
|
290
|
+
raise SystemExit(f"--window: {e}")
|
|
291
|
+
if end_ms < start_ms:
|
|
292
|
+
raise SystemExit("--window: end_ms must be >= start_ms")
|
|
293
|
+
return (int(start_ms * 1_000_000), int(end_ms * 1_000_000))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _strip_internal(lane: dict) -> dict:
|
|
297
|
+
return {k: v for k, v in lane.items() if not k.startswith("_")}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Parsers for Xcode Instruments .trace files via xctrace export."""
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""SwiftUI cause-graph lane (`swiftui-causes` schema).
|
|
2
|
+
|
|
3
|
+
Instruments emits one row per edge in SwiftUI's dependency graph: every time
|
|
4
|
+
a source node (a state change, user defaults observer, system event, etc.)
|
|
5
|
+
propagates to a destination node (a body evaluation, layout, creation), a
|
|
6
|
+
row is written with both endpoints as metadata values.
|
|
7
|
+
|
|
8
|
+
This lane aggregates those edges two ways:
|
|
9
|
+
|
|
10
|
+
- **By source node** — which attribute graph nodes are driving the most
|
|
11
|
+
updates overall. The canonical "why is my app thrashing?" view; a
|
|
12
|
+
`UserDefaultObserver.send()` showing up with 11k outgoing edges is a
|
|
13
|
+
feedback storm.
|
|
14
|
+
- **By destination node** — which views/modifiers receive the most
|
|
15
|
+
invalidations, and from whom. Use this to trace a hot view back to the
|
|
16
|
+
source that keeps poking it.
|
|
17
|
+
|
|
18
|
+
The analyzer's main lane (`swiftui`) tells you *what* updates are
|
|
19
|
+
expensive; this lane tells you *why* they keep happening.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections import Counter, defaultdict
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from . import xctrace, xml_utils
|
|
28
|
+
|
|
29
|
+
SCHEMA = "swiftui-causes"
|
|
30
|
+
|
|
31
|
+
# Metadata nodes render as space-separated field dumps ("A gray icon n/a n/a").
|
|
32
|
+
# We aggregate on the full fmt string so callers can spot specific edges like
|
|
33
|
+
# "@AppStorage TextStyleModifier.fontOption", but also expose the short head
|
|
34
|
+
# ("@AppStorage", "Creation of App", ...) for coarser grouping.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def analyze(
|
|
38
|
+
trace_path: Path,
|
|
39
|
+
toc_schemas: frozenset[str],
|
|
40
|
+
top_n: int = 10,
|
|
41
|
+
top_k_per_node: int = 5,
|
|
42
|
+
window: tuple[int, int] | None = None,
|
|
43
|
+
run: int = 1,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
if SCHEMA not in toc_schemas:
|
|
46
|
+
return {
|
|
47
|
+
"lane": "swiftui-causes",
|
|
48
|
+
"available": False,
|
|
49
|
+
"notes": [
|
|
50
|
+
"SwiftUI causes data not present (requires SwiftUI template on a real device).",
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
xml_bytes = xctrace.export_schema(trace_path, SCHEMA, run=run)
|
|
55
|
+
stream = xml_utils.RowStream(xml_bytes)
|
|
56
|
+
|
|
57
|
+
source_edges: Counter[str] = Counter()
|
|
58
|
+
destination_edges: Counter[str] = Counter()
|
|
59
|
+
fanout: dict[str, Counter[str]] = defaultdict(Counter)
|
|
60
|
+
fanin: dict[str, Counter[str]] = defaultdict(Counter)
|
|
61
|
+
label_counts: Counter[str] = Counter()
|
|
62
|
+
total_edges = 0
|
|
63
|
+
|
|
64
|
+
for row in stream:
|
|
65
|
+
time_el = xml_utils.first_present(row, "timestamp", "time")
|
|
66
|
+
if time_el is not None:
|
|
67
|
+
t_ns = xml_utils.int_text(stream.resolve(time_el))
|
|
68
|
+
if t_ns is not None and not xml_utils.in_window(t_ns, window):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
src = _fmt(row, stream, "source-node")
|
|
72
|
+
dst = _fmt(row, stream, "destination-node")
|
|
73
|
+
if not src or not dst:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
source_edges[src] += 1
|
|
77
|
+
destination_edges[dst] += 1
|
|
78
|
+
fanout[src][dst] += 1
|
|
79
|
+
fanin[dst][src] += 1
|
|
80
|
+
|
|
81
|
+
label = _fmt(row, stream, "label")
|
|
82
|
+
if label:
|
|
83
|
+
label_counts[label] += 1
|
|
84
|
+
|
|
85
|
+
total_edges += 1
|
|
86
|
+
|
|
87
|
+
top_sources = [
|
|
88
|
+
{
|
|
89
|
+
"source": src,
|
|
90
|
+
"edges": count,
|
|
91
|
+
"top_destinations": [
|
|
92
|
+
{"destination": d, "edges": c}
|
|
93
|
+
for d, c in fanout[src].most_common(top_k_per_node)
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
for src, count in source_edges.most_common(top_n)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
top_destinations = [
|
|
100
|
+
{
|
|
101
|
+
"destination": dst,
|
|
102
|
+
"edges": count,
|
|
103
|
+
"top_sources": [
|
|
104
|
+
{"source": s, "edges": c}
|
|
105
|
+
for s, c in fanin[dst].most_common(top_k_per_node)
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
for dst, count in destination_edges.most_common(top_n)
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"lane": "swiftui-causes",
|
|
113
|
+
"available": True,
|
|
114
|
+
"schema_used": SCHEMA,
|
|
115
|
+
"metrics": {
|
|
116
|
+
"total_edges": total_edges,
|
|
117
|
+
"unique_sources": len(source_edges),
|
|
118
|
+
"unique_destinations": len(destination_edges),
|
|
119
|
+
"top_labels": dict(label_counts.most_common(top_n)),
|
|
120
|
+
},
|
|
121
|
+
"top_sources": top_sources,
|
|
122
|
+
"top_destinations": top_destinations,
|
|
123
|
+
"notes": [],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def fanin_for(
|
|
128
|
+
trace_path: Path,
|
|
129
|
+
toc_schemas: frozenset[str],
|
|
130
|
+
destination_contains: str,
|
|
131
|
+
top_k: int = 10,
|
|
132
|
+
window: tuple[int, int] | None = None,
|
|
133
|
+
run: int = 1,
|
|
134
|
+
) -> dict[str, Any]:
|
|
135
|
+
"""Return the top source nodes feeding any destination whose fmt string
|
|
136
|
+
contains `destination_contains` (case-insensitive substring).
|
|
137
|
+
|
|
138
|
+
Used when the agent has a suspect view from the `swiftui` lane and wants
|
|
139
|
+
to know *who keeps invalidating it*. Does a full pass over the causes
|
|
140
|
+
schema each time — cheap enough at typical trace sizes.
|
|
141
|
+
"""
|
|
142
|
+
if SCHEMA not in toc_schemas:
|
|
143
|
+
return {"available": False, "matches": []}
|
|
144
|
+
|
|
145
|
+
needle = destination_contains.lower()
|
|
146
|
+
xml_bytes = xctrace.export_schema(trace_path, SCHEMA, run=run)
|
|
147
|
+
stream = xml_utils.RowStream(xml_bytes)
|
|
148
|
+
|
|
149
|
+
matches: dict[str, Counter[str]] = defaultdict(Counter)
|
|
150
|
+
totals: Counter[str] = Counter()
|
|
151
|
+
|
|
152
|
+
for row in stream:
|
|
153
|
+
time_el = xml_utils.first_present(row, "timestamp", "time")
|
|
154
|
+
if time_el is not None:
|
|
155
|
+
t_ns = xml_utils.int_text(stream.resolve(time_el))
|
|
156
|
+
if t_ns is not None and not xml_utils.in_window(t_ns, window):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
dst = _fmt(row, stream, "destination-node")
|
|
160
|
+
if not dst or needle not in dst.lower():
|
|
161
|
+
continue
|
|
162
|
+
src = _fmt(row, stream, "source-node")
|
|
163
|
+
if not src:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
matches[dst][src] += 1
|
|
167
|
+
totals[dst] += 1
|
|
168
|
+
|
|
169
|
+
out = []
|
|
170
|
+
for dst, count in totals.most_common(top_k):
|
|
171
|
+
out.append({
|
|
172
|
+
"destination": dst,
|
|
173
|
+
"total_incoming_edges": count,
|
|
174
|
+
"top_sources": [
|
|
175
|
+
{"source": s, "edges": c}
|
|
176
|
+
for s, c in matches[dst].most_common(top_k)
|
|
177
|
+
],
|
|
178
|
+
})
|
|
179
|
+
return {"available": True, "matches": out}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _fmt(row, stream, key: str) -> str | None:
|
|
183
|
+
el = row.get(key)
|
|
184
|
+
if el is None:
|
|
185
|
+
return None
|
|
186
|
+
resolved = stream.resolve(el)
|
|
187
|
+
return resolved.get("fmt") or xml_utils.str_text(resolved)
|