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,179 @@
|
|
|
1
|
+
"""Cross-lane correlation: for each hang and top-N worst hitches, aggregate
|
|
2
|
+
Time Profiler samples and SwiftUI updates whose timestamps fall inside the
|
|
3
|
+
event window [start, start+duration]. Uses bisect so lookups stay O(log N)
|
|
4
|
+
per event.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from bisect import bisect_left, bisect_right
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build(lanes: dict[str, dict], top_hitches: int = 5, top_symbols: int = 5) -> list[dict]:
|
|
14
|
+
"""Produce a list of correlation entries.
|
|
15
|
+
|
|
16
|
+
`lanes` is a dict keyed by lane name (time-profiler, hangs, hitches,
|
|
17
|
+
swiftui) of their analyzer outputs.
|
|
18
|
+
"""
|
|
19
|
+
tp = lanes.get("time-profiler")
|
|
20
|
+
hangs = lanes.get("hangs")
|
|
21
|
+
hitches = lanes.get("hitches")
|
|
22
|
+
swiftui = lanes.get("swiftui")
|
|
23
|
+
|
|
24
|
+
tp_index = _build_time_profile_index(tp)
|
|
25
|
+
sui_events = (swiftui or {}).get("_events") if swiftui and swiftui.get("available") else None
|
|
26
|
+
|
|
27
|
+
correlations: list[dict] = []
|
|
28
|
+
|
|
29
|
+
if hangs and hangs.get("available"):
|
|
30
|
+
for h in hangs.get("_events", []):
|
|
31
|
+
correlations.append(
|
|
32
|
+
_correlate_event(
|
|
33
|
+
trigger_lane="hangs",
|
|
34
|
+
start_ns=h["start_ns"],
|
|
35
|
+
end_ns=h["end_ns"],
|
|
36
|
+
extra={"hang_type": h["hang_type"]},
|
|
37
|
+
tp_index=tp_index,
|
|
38
|
+
sui_events=sui_events,
|
|
39
|
+
top_symbols=top_symbols,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if hitches and hitches.get("available"):
|
|
44
|
+
worst_hitches = hitches.get("_events", [])[:top_hitches]
|
|
45
|
+
for hi in worst_hitches:
|
|
46
|
+
correlations.append(
|
|
47
|
+
_correlate_event(
|
|
48
|
+
trigger_lane="hitches",
|
|
49
|
+
start_ns=hi["start_ns"],
|
|
50
|
+
end_ns=hi["end_ns"],
|
|
51
|
+
extra={
|
|
52
|
+
"frame_duration_ms": hi["frame_duration_ms"],
|
|
53
|
+
"hitch_duration_ms": hi["hitch_duration_ms"],
|
|
54
|
+
},
|
|
55
|
+
tp_index=tp_index,
|
|
56
|
+
sui_events=sui_events,
|
|
57
|
+
top_symbols=top_symbols,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return correlations
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --- Internal -------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def _build_time_profile_index(tp: dict | None):
|
|
67
|
+
if not tp or not tp.get("available"):
|
|
68
|
+
return None
|
|
69
|
+
samples = tp.get("_samples") or []
|
|
70
|
+
if not samples:
|
|
71
|
+
return None
|
|
72
|
+
# Samples are already sorted by time in time_profiler.analyze.
|
|
73
|
+
times = [s["time_ns"] for s in samples]
|
|
74
|
+
return {"times": times, "samples": samples}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _correlate_event(
|
|
78
|
+
trigger_lane: str,
|
|
79
|
+
start_ns: int,
|
|
80
|
+
end_ns: int,
|
|
81
|
+
extra: dict,
|
|
82
|
+
tp_index: dict | None,
|
|
83
|
+
sui_events: list[dict] | None,
|
|
84
|
+
top_symbols: int,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
entry: dict[str, Any] = {
|
|
87
|
+
"trigger": {
|
|
88
|
+
"lane": trigger_lane,
|
|
89
|
+
"start_ms": round(start_ns / 1_000_000, 2),
|
|
90
|
+
"end_ms": round(end_ns / 1_000_000, 2),
|
|
91
|
+
"duration_ms": round((end_ns - start_ns) / 1_000_000, 2),
|
|
92
|
+
**extra,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if tp_index is not None:
|
|
97
|
+
tp = _time_profile_hot_symbols(
|
|
98
|
+
tp_index, start_ns, end_ns, top_symbols
|
|
99
|
+
)
|
|
100
|
+
duration_ns = end_ns - start_ns
|
|
101
|
+
# Sample rate is 1ms/sample on standard Time Profiler. If the window
|
|
102
|
+
# is N ms long we'd expect ~N main-thread samples if main was fully
|
|
103
|
+
# running; fewer means main was blocked (I/O, lock, etc.).
|
|
104
|
+
expected_if_running = max(1, duration_ns // 1_000_000)
|
|
105
|
+
coverage_pct = min(100.0, 100.0 * tp["samples_main"] / expected_if_running)
|
|
106
|
+
entry["time_profiler_main_thread"] = {
|
|
107
|
+
"samples_in_window": tp["samples_total"],
|
|
108
|
+
"samples_on_main": tp["samples_main"],
|
|
109
|
+
"main_running_coverage_pct": round(coverage_pct, 1),
|
|
110
|
+
"hot_symbols": tp["hot_symbols"],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if sui_events is not None:
|
|
114
|
+
sui_overlap = _swiftui_overlaps(sui_events, start_ns, end_ns)
|
|
115
|
+
entry["swiftui_overlapping_updates"] = sui_overlap
|
|
116
|
+
|
|
117
|
+
return entry
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _time_profile_hot_symbols(
|
|
121
|
+
tp_index: dict, start_ns: int, end_ns: int, top_n: int
|
|
122
|
+
) -> dict:
|
|
123
|
+
"""Return main-thread hot symbols in the given window.
|
|
124
|
+
|
|
125
|
+
Hang/hitch/SwiftUI correlations are all main-thread responsiveness
|
|
126
|
+
problems, so worker-thread symbols are noise. We also return a coverage
|
|
127
|
+
metric — when main was blocked on I/O or a lock, the window will have
|
|
128
|
+
far fewer samples than its duration would predict, and that signal is
|
|
129
|
+
what tells the agent "this was blocked, not CPU-bound".
|
|
130
|
+
"""
|
|
131
|
+
times = tp_index["times"]
|
|
132
|
+
samples = tp_index["samples"]
|
|
133
|
+
lo = bisect_left(times, start_ns)
|
|
134
|
+
hi = bisect_right(times, end_ns)
|
|
135
|
+
window = samples[lo:hi]
|
|
136
|
+
if not window:
|
|
137
|
+
return {"samples_total": 0, "samples_main": 0, "hot_symbols": []}
|
|
138
|
+
|
|
139
|
+
main_samples = [s for s in window if s["is_main"]]
|
|
140
|
+
weight_by_symbol: dict[str, int] = defaultdict(int)
|
|
141
|
+
count_by_symbol: dict[str, int] = defaultdict(int)
|
|
142
|
+
for s in main_samples:
|
|
143
|
+
weight_by_symbol[s["leaf_symbol"]] += s["weight_ns"]
|
|
144
|
+
count_by_symbol[s["leaf_symbol"]] += 1
|
|
145
|
+
total_weight = sum(weight_by_symbol.values()) or 1
|
|
146
|
+
|
|
147
|
+
ranked = sorted(weight_by_symbol.items(), key=lambda kv: kv[1], reverse=True)
|
|
148
|
+
hot = []
|
|
149
|
+
for symbol, weight in ranked[:top_n]:
|
|
150
|
+
hot.append({
|
|
151
|
+
"symbol": symbol,
|
|
152
|
+
"samples": count_by_symbol[symbol],
|
|
153
|
+
"weight_ms": round(weight / 1_000_000, 2),
|
|
154
|
+
"percent_of_main": round(100.0 * weight / total_weight, 2),
|
|
155
|
+
})
|
|
156
|
+
return {
|
|
157
|
+
"samples_total": len(window),
|
|
158
|
+
"samples_main": len(main_samples),
|
|
159
|
+
"hot_symbols": hot,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _swiftui_overlaps(
|
|
164
|
+
events: list[dict], start_ns: int, end_ns: int
|
|
165
|
+
) -> list[dict]:
|
|
166
|
+
# Events aren't guaranteed sorted by start_ns here (we sort by duration in
|
|
167
|
+
# swiftui.analyze). Linear scan; SwiftUI event counts are typically small.
|
|
168
|
+
out: list[dict] = []
|
|
169
|
+
for e in events:
|
|
170
|
+
if e["end_ns"] < start_ns or e["start_ns"] > end_ns:
|
|
171
|
+
continue
|
|
172
|
+
out.append({
|
|
173
|
+
"view": e["view"],
|
|
174
|
+
"duration_ms": e["duration_ms"],
|
|
175
|
+
"start_ms": e["start_ms"],
|
|
176
|
+
})
|
|
177
|
+
# Worst first.
|
|
178
|
+
out.sort(key=lambda x: x["duration_ms"], reverse=True)
|
|
179
|
+
return out[:10]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Discovery helpers for os_log messages and os_signpost intervals.
|
|
2
|
+
|
|
3
|
+
These let an agent locate a focus window (e.g. "after the log saying X",
|
|
4
|
+
"during signpost Y") before running the main lane analysis.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import xctrace, xml_utils
|
|
12
|
+
|
|
13
|
+
OS_LOG_SCHEMA = "os-log"
|
|
14
|
+
OS_SIGNPOST_SCHEMA = "os-signpost"
|
|
15
|
+
OS_SIGNPOST_INTERVAL_SCHEMA = "os-signpost-interval"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_logs(
|
|
19
|
+
trace_path: Path,
|
|
20
|
+
toc_schemas: frozenset[str],
|
|
21
|
+
subsystem: str | None = None,
|
|
22
|
+
category: str | None = None,
|
|
23
|
+
message_contains: str | None = None,
|
|
24
|
+
message_type: str | None = None,
|
|
25
|
+
limit: int | None = None,
|
|
26
|
+
window_ns: tuple[int, int] | None = None,
|
|
27
|
+
run: int = 1,
|
|
28
|
+
) -> list[dict[str, Any]]:
|
|
29
|
+
"""Return os_log entries, optionally filtered. Case-insensitive contains.
|
|
30
|
+
|
|
31
|
+
`limit` counts *post-filter* matches — including the window filter — so
|
|
32
|
+
the caller gets N matching logs inside the window rather than the first
|
|
33
|
+
N matching logs that might all fall outside it.
|
|
34
|
+
"""
|
|
35
|
+
if OS_LOG_SCHEMA not in toc_schemas:
|
|
36
|
+
return []
|
|
37
|
+
xml_bytes = xctrace.export_schema(trace_path, OS_LOG_SCHEMA, run=run)
|
|
38
|
+
stream = xml_utils.RowStream(xml_bytes)
|
|
39
|
+
needle = message_contains.lower() if message_contains else None
|
|
40
|
+
|
|
41
|
+
out: list[dict[str, Any]] = []
|
|
42
|
+
for row in stream:
|
|
43
|
+
time_el = row.get("time")
|
|
44
|
+
if time_el is None:
|
|
45
|
+
continue
|
|
46
|
+
time_ns = xml_utils.int_text(stream.resolve(time_el))
|
|
47
|
+
if time_ns is None:
|
|
48
|
+
continue
|
|
49
|
+
if not xml_utils.in_window(time_ns, window_ns):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
sub = _str_of(row, stream, "subsystem")
|
|
53
|
+
cat = _str_of(row, stream, "category")
|
|
54
|
+
typ = _str_of(row, stream, "message-type")
|
|
55
|
+
fmt = _str_of(row, stream, "format-string")
|
|
56
|
+
msg = _str_of(row, stream, "message") or fmt
|
|
57
|
+
|
|
58
|
+
if subsystem and (sub or "") != subsystem:
|
|
59
|
+
continue
|
|
60
|
+
if category and (cat or "") != category:
|
|
61
|
+
continue
|
|
62
|
+
if message_type and (typ or "") != message_type:
|
|
63
|
+
continue
|
|
64
|
+
if needle and needle not in (msg or "").lower() and needle not in (fmt or "").lower():
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
process_el = row.get("process")
|
|
68
|
+
process = (
|
|
69
|
+
xml_utils.extract_process(process_el, stream).get("name")
|
|
70
|
+
if process_el is not None else None
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
out.append({
|
|
74
|
+
"time_ns": time_ns,
|
|
75
|
+
"time_ms": round(time_ns / 1_000_000, 3),
|
|
76
|
+
"type": typ,
|
|
77
|
+
"subsystem": sub,
|
|
78
|
+
"category": cat,
|
|
79
|
+
"process": process,
|
|
80
|
+
"message": msg,
|
|
81
|
+
"format_string": fmt,
|
|
82
|
+
})
|
|
83
|
+
if limit is not None and len(out) >= limit:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
out.sort(key=lambda e: e["time_ns"])
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def list_signposts(
|
|
91
|
+
trace_path: Path,
|
|
92
|
+
toc_schemas: frozenset[str],
|
|
93
|
+
name_contains: str | None = None,
|
|
94
|
+
subsystem: str | None = None,
|
|
95
|
+
category: str | None = None,
|
|
96
|
+
window_ns: tuple[int, int] | None = None,
|
|
97
|
+
run: int = 1,
|
|
98
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
99
|
+
"""Return signpost intervals (paired begin/end) plus single-point events.
|
|
100
|
+
|
|
101
|
+
Shape: { "intervals": [...], "events": [...] }. Intervals have
|
|
102
|
+
start_ms/end_ms/duration_ms; events have a single time_ms.
|
|
103
|
+
|
|
104
|
+
Reads two complementary schemas:
|
|
105
|
+
* `os-signpost-interval`: already-paired intervals (this is where
|
|
106
|
+
user-emitted signposts like com.example.MyApp typically land).
|
|
107
|
+
* `os-signpost`: raw begin/end/event rows; we pair begins with ends
|
|
108
|
+
ourselves and fall back to point events for unpaired rows. Most
|
|
109
|
+
Apple-framework signposts (CloudKit, AppKit, …) live here.
|
|
110
|
+
|
|
111
|
+
Filters are AND-combined. `name_contains` is a case-insensitive substring
|
|
112
|
+
match. `window_ns` keeps intervals that overlap the window (not strict
|
|
113
|
+
containment) and point events whose timestamp falls inside it.
|
|
114
|
+
"""
|
|
115
|
+
# The two signpost schemas overlap: every paired begin/end in `os-signpost`
|
|
116
|
+
# also shows up as a row in `os-signpost-interval`. To avoid duplicates we
|
|
117
|
+
# prefer the pre-paired schema for intervals and only mine `os-signpost`
|
|
118
|
+
# for point events (and for begin/end pairing as a fallback when the
|
|
119
|
+
# interval schema is missing — older traces).
|
|
120
|
+
intervals: list[dict[str, Any]] = []
|
|
121
|
+
events: list[dict[str, Any]] = []
|
|
122
|
+
|
|
123
|
+
has_intervals = OS_SIGNPOST_INTERVAL_SCHEMA in toc_schemas
|
|
124
|
+
if has_intervals:
|
|
125
|
+
intervals.extend(_read_interval_schema(trace_path, run=run))
|
|
126
|
+
|
|
127
|
+
if OS_SIGNPOST_SCHEMA in toc_schemas:
|
|
128
|
+
more_intervals, more_events = _read_event_schema(trace_path, run=run)
|
|
129
|
+
if not has_intervals:
|
|
130
|
+
intervals.extend(more_intervals)
|
|
131
|
+
events.extend(more_events)
|
|
132
|
+
|
|
133
|
+
intervals.sort(key=lambda i: i["start_ns"])
|
|
134
|
+
events.sort(key=lambda e: e["time_ns"])
|
|
135
|
+
|
|
136
|
+
needle = name_contains.lower() if name_contains else None
|
|
137
|
+
|
|
138
|
+
def _matches(entry: dict) -> bool:
|
|
139
|
+
if subsystem and (entry.get("subsystem") or "") != subsystem:
|
|
140
|
+
return False
|
|
141
|
+
if category and (entry.get("category") or "") != category:
|
|
142
|
+
return False
|
|
143
|
+
if needle and needle not in (entry.get("name") or "").lower():
|
|
144
|
+
return False
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
if subsystem or category or needle:
|
|
148
|
+
intervals = [i for i in intervals if _matches(i)]
|
|
149
|
+
events = [e for e in events if _matches(e)]
|
|
150
|
+
|
|
151
|
+
if window_ns is not None:
|
|
152
|
+
s, e = window_ns
|
|
153
|
+
intervals = [
|
|
154
|
+
i for i in intervals
|
|
155
|
+
if not (i["end_ns"] < s or i["start_ns"] > e)
|
|
156
|
+
]
|
|
157
|
+
events = [ev for ev in events if s <= ev["time_ns"] <= e]
|
|
158
|
+
|
|
159
|
+
return {"intervals": intervals, "events": events}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _read_interval_schema(trace_path: Path, run: int = 1) -> list[dict[str, Any]]:
|
|
163
|
+
"""Read the os-signpost-interval schema (pre-paired intervals)."""
|
|
164
|
+
xml_bytes = xctrace.export_schema(trace_path, OS_SIGNPOST_INTERVAL_SCHEMA, run=run)
|
|
165
|
+
stream = xml_utils.RowStream(xml_bytes)
|
|
166
|
+
|
|
167
|
+
out: list[dict[str, Any]] = []
|
|
168
|
+
for row in stream:
|
|
169
|
+
start_el = xml_utils.first_present(row, "start", "time")
|
|
170
|
+
dur_el = row.get("duration")
|
|
171
|
+
if start_el is None or dur_el is None:
|
|
172
|
+
continue
|
|
173
|
+
start_ns = xml_utils.int_text(stream.resolve(start_el))
|
|
174
|
+
dur_ns = xml_utils.int_text(stream.resolve(dur_el))
|
|
175
|
+
if start_ns is None or dur_ns is None:
|
|
176
|
+
continue
|
|
177
|
+
end_ns = start_ns + dur_ns
|
|
178
|
+
|
|
179
|
+
name = _str_of(row, stream, "name")
|
|
180
|
+
sub = _str_of(row, stream, "subsystem")
|
|
181
|
+
cat = _str_of(row, stream, "category")
|
|
182
|
+
signpost_id = _str_of(row, stream, "identifier") or _str_of(row, stream, "signpost-id")
|
|
183
|
+
process_el = row.get("process")
|
|
184
|
+
process = (
|
|
185
|
+
xml_utils.extract_process(process_el, stream).get("name")
|
|
186
|
+
if process_el is not None else None
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
out.append({
|
|
190
|
+
"start_ns": start_ns,
|
|
191
|
+
"end_ns": end_ns,
|
|
192
|
+
"duration_ns": dur_ns,
|
|
193
|
+
"start_ms": round(start_ns / 1_000_000, 3),
|
|
194
|
+
"end_ms": round(end_ns / 1_000_000, 3),
|
|
195
|
+
"duration_ms": round(dur_ns / 1_000_000, 3),
|
|
196
|
+
"name": name,
|
|
197
|
+
"subsystem": sub,
|
|
198
|
+
"category": cat,
|
|
199
|
+
"process": process,
|
|
200
|
+
"signpost_id": signpost_id,
|
|
201
|
+
})
|
|
202
|
+
return out
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _read_event_schema(
|
|
206
|
+
trace_path: Path,
|
|
207
|
+
run: int = 1,
|
|
208
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
209
|
+
"""Read the os-signpost schema and pair begin/end rows into intervals."""
|
|
210
|
+
xml_bytes = xctrace.export_schema(trace_path, OS_SIGNPOST_SCHEMA, run=run)
|
|
211
|
+
stream = xml_utils.RowStream(xml_bytes)
|
|
212
|
+
|
|
213
|
+
pending: dict[tuple, dict] = {}
|
|
214
|
+
intervals: list[dict[str, Any]] = []
|
|
215
|
+
events: list[dict[str, Any]] = []
|
|
216
|
+
|
|
217
|
+
for row in stream:
|
|
218
|
+
time_el = xml_utils.first_present(row, "time", "start")
|
|
219
|
+
if time_el is None:
|
|
220
|
+
continue
|
|
221
|
+
time_ns = xml_utils.int_text(stream.resolve(time_el))
|
|
222
|
+
if time_ns is None:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
name = _str_of(row, stream, "name")
|
|
226
|
+
sub = _str_of(row, stream, "subsystem")
|
|
227
|
+
cat = _str_of(row, stream, "category")
|
|
228
|
+
event_type = _str_of(row, stream, "event-type") or _str_of(row, stream, "message-type")
|
|
229
|
+
signpost_id = _str_of(row, stream, "signpost-id") or _str_of(row, stream, "identifier")
|
|
230
|
+
process_el = row.get("process")
|
|
231
|
+
process = (
|
|
232
|
+
xml_utils.extract_process(process_el, stream).get("name")
|
|
233
|
+
if process_el is not None else None
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
key = (process, sub, cat, name, signpost_id)
|
|
237
|
+
etype = (event_type or "").lower()
|
|
238
|
+
|
|
239
|
+
if etype in ("begin", "interval begin", "start"):
|
|
240
|
+
pending[key] = {"start_ns": time_ns, "name": name,
|
|
241
|
+
"subsystem": sub, "category": cat,
|
|
242
|
+
"process": process, "signpost_id": signpost_id}
|
|
243
|
+
elif etype in ("end", "interval end", "stop"):
|
|
244
|
+
start = pending.pop(key, None)
|
|
245
|
+
if start is not None:
|
|
246
|
+
dur_ns = time_ns - start["start_ns"]
|
|
247
|
+
intervals.append({
|
|
248
|
+
**start,
|
|
249
|
+
"end_ns": time_ns,
|
|
250
|
+
"duration_ns": dur_ns,
|
|
251
|
+
"start_ms": round(start["start_ns"] / 1_000_000, 3),
|
|
252
|
+
"end_ms": round(time_ns / 1_000_000, 3),
|
|
253
|
+
"duration_ms": round(dur_ns / 1_000_000, 3),
|
|
254
|
+
})
|
|
255
|
+
else:
|
|
256
|
+
events.append(_point_event(time_ns, name, sub, cat,
|
|
257
|
+
process, signpost_id, event_type))
|
|
258
|
+
else:
|
|
259
|
+
events.append(_point_event(time_ns, name, sub, cat,
|
|
260
|
+
process, signpost_id, event_type))
|
|
261
|
+
|
|
262
|
+
# Unclosed begins are surfaced as point events so nothing is silently dropped.
|
|
263
|
+
for info in pending.values():
|
|
264
|
+
events.append(_point_event(info["start_ns"], info["name"],
|
|
265
|
+
info["subsystem"], info["category"],
|
|
266
|
+
info["process"], info["signpost_id"],
|
|
267
|
+
"Begin (unclosed)"))
|
|
268
|
+
|
|
269
|
+
return intervals, events
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _point_event(time_ns, name, subsystem, category, process, signpost_id, event_type):
|
|
273
|
+
return {
|
|
274
|
+
"time_ns": time_ns,
|
|
275
|
+
"time_ms": round(time_ns / 1_000_000, 3),
|
|
276
|
+
"name": name,
|
|
277
|
+
"subsystem": subsystem,
|
|
278
|
+
"category": category,
|
|
279
|
+
"process": process,
|
|
280
|
+
"signpost_id": signpost_id,
|
|
281
|
+
"event_type": event_type,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _str_of(row, stream, key):
|
|
286
|
+
el = row.get(key)
|
|
287
|
+
if el is None:
|
|
288
|
+
return None
|
|
289
|
+
resolved = stream.resolve(el)
|
|
290
|
+
txt = xml_utils.str_text(resolved) or resolved.get("fmt")
|
|
291
|
+
return txt
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Hangs lane parser (schema `potential-hangs`).
|
|
2
|
+
|
|
3
|
+
The schema lacks inline backtraces — stacks come from Time Profiler samples
|
|
4
|
+
that overlap each hang's window. Correlation is done later in correlate.py.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import xctrace, xml_utils
|
|
12
|
+
|
|
13
|
+
PREFERRED_SCHEMAS = ("potential-hangs",)
|
|
14
|
+
FALLBACK_SCHEMAS = ("main-thread-hang", "hang", "hangs")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def analyze(
|
|
18
|
+
trace_path: Path,
|
|
19
|
+
toc_schemas: frozenset[str],
|
|
20
|
+
top_n: int = 10,
|
|
21
|
+
window: tuple[int, int] | None = None,
|
|
22
|
+
run: int = 1,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
schema = _pick_schema(toc_schemas)
|
|
25
|
+
if schema is None:
|
|
26
|
+
return {
|
|
27
|
+
"lane": "hangs",
|
|
28
|
+
"available": False,
|
|
29
|
+
"notes": ["Hangs data not present in trace."],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
xml_bytes = xctrace.export_schema(trace_path, schema, run=run)
|
|
33
|
+
stream = xml_utils.RowStream(xml_bytes)
|
|
34
|
+
|
|
35
|
+
hangs: list[dict] = []
|
|
36
|
+
for row in stream:
|
|
37
|
+
start_el = row.get("start")
|
|
38
|
+
dur_el = row.get("duration")
|
|
39
|
+
type_el = row.get("hang-type")
|
|
40
|
+
thread_el = row.get("thread")
|
|
41
|
+
if start_el is None or dur_el is None:
|
|
42
|
+
continue
|
|
43
|
+
start_ns = xml_utils.int_text(stream.resolve(start_el))
|
|
44
|
+
duration_ns = xml_utils.int_text(stream.resolve(dur_el))
|
|
45
|
+
if start_ns is None or duration_ns is None:
|
|
46
|
+
continue
|
|
47
|
+
if not xml_utils.event_overlaps_window(start_ns, start_ns + duration_ns, window):
|
|
48
|
+
continue
|
|
49
|
+
hang_type = xml_utils.str_text(stream.resolve(type_el)) if type_el is not None else None
|
|
50
|
+
thread = xml_utils.extract_thread(thread_el, stream) if thread_el is not None else None
|
|
51
|
+
|
|
52
|
+
hangs.append({
|
|
53
|
+
"start_ns": start_ns,
|
|
54
|
+
"duration_ns": duration_ns,
|
|
55
|
+
"end_ns": start_ns + duration_ns,
|
|
56
|
+
"duration_ms": round(duration_ns / 1_000_000, 2),
|
|
57
|
+
"start_ms": round(start_ns / 1_000_000, 2),
|
|
58
|
+
"hang_type": hang_type or "Hang",
|
|
59
|
+
"thread": thread,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
hangs.sort(key=lambda h: h["duration_ns"], reverse=True)
|
|
63
|
+
|
|
64
|
+
total_ms = sum(h["duration_ms"] for h in hangs)
|
|
65
|
+
worst = hangs[0] if hangs else None
|
|
66
|
+
|
|
67
|
+
# Severity buckets per Apple docs (Microhang: 250ms–500ms, Hang: ≥500ms).
|
|
68
|
+
# We bucket by raw duration so the agent can reason about it.
|
|
69
|
+
buckets = {"lt_250ms": 0, "250ms_1s": 0, "gt_1s": 0}
|
|
70
|
+
for h in hangs:
|
|
71
|
+
if h["duration_ms"] < 250:
|
|
72
|
+
buckets["lt_250ms"] += 1
|
|
73
|
+
elif h["duration_ms"] < 1000:
|
|
74
|
+
buckets["250ms_1s"] += 1
|
|
75
|
+
else:
|
|
76
|
+
buckets["gt_1s"] += 1
|
|
77
|
+
|
|
78
|
+
top_offenders = [
|
|
79
|
+
{
|
|
80
|
+
"start_ms": h["start_ms"],
|
|
81
|
+
"duration_ms": h["duration_ms"],
|
|
82
|
+
"hang_type": h["hang_type"],
|
|
83
|
+
"thread": (h["thread"] or {}).get("name", ""),
|
|
84
|
+
}
|
|
85
|
+
for h in hangs[:top_n]
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"lane": "hangs",
|
|
90
|
+
"available": True,
|
|
91
|
+
"schema_used": schema,
|
|
92
|
+
"metrics": {
|
|
93
|
+
"count": len(hangs),
|
|
94
|
+
"total_duration_ms": round(total_ms, 2),
|
|
95
|
+
"worst_duration_ms": worst["duration_ms"] if worst else 0,
|
|
96
|
+
"severity_buckets": buckets,
|
|
97
|
+
},
|
|
98
|
+
"top_offenders": top_offenders,
|
|
99
|
+
"notes": [],
|
|
100
|
+
"_events": hangs, # retained for correlation
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _pick_schema(available: frozenset[str]) -> str | None:
|
|
105
|
+
for s in PREFERRED_SCHEMAS + FALLBACK_SCHEMAS:
|
|
106
|
+
if s in available:
|
|
107
|
+
return s
|
|
108
|
+
return None
|