Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/analyze_trace.py
1#!/usr/bin/env python32"""Analyze an Xcode Instruments .trace file and emit JSON + markdown.34Primary modes:5(default) Full four-lane analysis + cross-lane correlations.6--list-logs Dump os_log entries (optionally filtered) as JSON so an7agent can locate a focus window by log content.8--list-signposts Dump os_signpost intervals + point events as JSON.910Windowing:11--window START_MS:END_MS restricts every lane to that slice of the trace.12"""13from __future__ import annotations1415import argparse16import json17import sys18from pathlib import Path1920from instruments_parser import (21causes,22correlate,23events,24hangs,25hitches,26summary,27swiftui,28time_profiler,29xctrace,30)313233def main(argv: list[str] | None = None) -> int:34parser = argparse.ArgumentParser(35description="Analyze an Instruments .trace file.",36)37parser.add_argument("--trace", required=True, type=Path)38parser.add_argument(39"--output",40type=Path,41help="Base path; writes <output>.json and <output>.md",42)43parser.add_argument("--top", type=int, default=10, help="Top-N per lane")44parser.add_argument(45"--top-hitches",46type=int,47default=5,48help="Correlate only the N worst hitches (avoid flooding output).",49)50parser.add_argument(51"--window",52type=str,53default=None,54help="Restrict analysis to a time slice, e.g. --window 10400:11700 (ms).",55)56parser.add_argument(57"--run",58type=int,59default=None,60help="Which run to analyze (1-based). Required for traces with >1 run.",61)62parser.add_argument(63"--list-runs", action="store_true",64help="Emit per-run metadata as JSON (use this to discover available runs).",65)6667# Mode flags (mutually exclusive with full analysis)68mode_group = parser.add_argument_group("Discovery modes")69mode_group.add_argument(70"--list-logs", action="store_true",71help="Emit os_log entries as JSON (use filter flags below).",72)73mode_group.add_argument(74"--list-signposts", action="store_true",75help="Emit os_signpost intervals + events as JSON.",76)77mode_group.add_argument("--log-subsystem", type=str, default=None)78mode_group.add_argument("--log-category", type=str, default=None)79mode_group.add_argument(80"--log-type", type=str, default=None,81help="e.g. Fault, Error, Default, Info, Debug",82)83mode_group.add_argument(84"--log-message-contains", type=str, default=None,85help="Case-insensitive substring match on the message / format string.",86)87mode_group.add_argument(88"--log-limit", type=int, default=None,89help="Cap number of log entries returned (applied after all filters).",90)91mode_group.add_argument(92"--signpost-name-contains", type=str, default=None,93help="Case-insensitive substring match on signpost name.",94)95mode_group.add_argument("--signpost-subsystem", type=str, default=None)96mode_group.add_argument("--signpost-category", type=str, default=None)97mode_group.add_argument(98"--fanin-for", type=str, default=None,99help="Emit incoming cause-graph sources for destinations whose fmt "100"contains this substring. Case-insensitive.",101)102103fmt_group = parser.add_mutually_exclusive_group()104fmt_group.add_argument("--json-only", action="store_true")105fmt_group.add_argument("--markdown-only", action="store_true")106107args = parser.parse_args(argv)108109# The discovery modes aren't in a mutually_exclusive_group because they110# live alongside their sub-filters in the same argparse group; enforce the111# constraint by hand so an agent gets a clear error instead of silent112# precedence.113active_modes = sum([114args.list_runs,115args.list_logs,116args.list_signposts,117bool(args.fanin_for),118])119if active_modes > 1:120parser.error(121"--list-runs, --list-logs, --list-signposts, and --fanin-for are "122"mutually exclusive; pick one per invocation."123)124125trace = args.trace126if not trace.exists():127print(f"error: trace not found: {trace}", file=sys.stderr)128return 2129130info = xctrace.toc(trace)131window_ns = _parse_window(args.window)132133if args.list_runs:134sys.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}145for r in info.runs146],147}, indent=2))148sys.stdout.write("\n")149return 0150151run_info = _resolve_run(info, args.run)152if run_info is None:153return 2154run_number = run_info.number155156if args.list_logs:157out = events.list_logs(158trace, run_info.schemas,159subsystem=args.log_subsystem,160category=args.log_category,161message_contains=args.log_message_contains,162message_type=args.log_type,163limit=args.log_limit,164window_ns=window_ns,165run=run_number,166)167sys.stdout.write(json.dumps({"logs": out, "count": len(out)}, indent=2))168sys.stdout.write("\n")169return 0170171if args.list_signposts:172sp = events.list_signposts(173trace, run_info.schemas,174name_contains=args.signpost_name_contains,175subsystem=args.signpost_subsystem,176category=args.signpost_category,177window_ns=window_ns,178run=run_number,179)180sys.stdout.write(json.dumps(sp, indent=2))181sys.stdout.write("\n")182return 0183184if args.fanin_for:185fanin = causes.fanin_for(186trace, run_info.schemas,187destination_contains=args.fanin_for,188top_k=args.top,189window=window_ns,190run=run_number,191)192sys.stdout.write(json.dumps(fanin, indent=2))193sys.stdout.write("\n")194return 0195196# Full five-lane analysis197schemas = run_info.schemas198lanes_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}205correlations = correlate.build(206lanes_out, top_hitches=args.top_hitches, top_symbols=5207)208public_lanes = [_strip_internal(l) for l in lanes_out.values()]209210result: 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}223if window_ns is not None:224result["window_ms"] = {225"start": window_ns[0] / 1_000_000,226"end": window_ns[1] / 1_000_000,227}228229md = summary.render(result)230231if args.output:232json_path = args.output.with_suffix(".json")233md_path = args.output.with_suffix(".md")234json_path.write_text(json.dumps(result, indent=2))235md_path.write_text(md)236print(f"wrote {json_path}")237print(f"wrote {md_path}")238return 0239240if args.markdown_only:241sys.stdout.write(md)242elif args.json_only:243sys.stdout.write(json.dumps(result, indent=2))244sys.stdout.write("\n")245else:246sys.stdout.write(json.dumps(result, indent=2))247sys.stdout.write("\n---\n")248sys.stdout.write(md)249return 0250251252def _resolve_run(info, requested: int | None):253"""Pick a run from the trace.254255If `requested` is given, return that run or None on miss (with a friendly256error). If unset and the trace has exactly one run, default to it. If257unset and there are multiple runs, error out so the agent picks258explicitly — silently picking run 1 lost data for the user.259"""260if not info.runs:261print("error: trace has no runs", file=sys.stderr)262return None263if requested is not None:264try:265return info.get_run(requested)266except KeyError as e:267print(f"error: {e}", file=sys.stderr)268return None269if len(info.runs) == 1:270return info.runs[0]271available = ", ".join(str(r.number) for r in info.runs)272print(273f"error: trace has {len(info.runs)} runs ({available}); pass --run N. "274f"Use --list-runs to see per-run metadata.",275file=sys.stderr,276)277return None278279280def _parse_window(spec: str | None) -> tuple[int, int] | None:281if not spec:282return None283if ":" not in spec:284raise SystemExit(f"--window expects START_MS:END_MS, got {spec!r}")285start_s, end_s = spec.split(":", 1)286try:287start_ms = float(start_s)288end_ms = float(end_s)289except ValueError as e:290raise SystemExit(f"--window: {e}")291if end_ms < start_ms:292raise SystemExit("--window: end_ms must be >= start_ms")293return (int(start_ms * 1_000_000), int(end_ms * 1_000_000))294295296def _strip_internal(lane: dict) -> dict:297return {k: v for k, v in lane.items() if not k.startswith("_")}298299300if __name__ == "__main__":301sys.exit(main())302