
Native App Performance
- 188 installs
- 6.5k repo stars
- Updated August 3, 2026
- steipete/agent-scripts
Use native-app-performance for development tasks
About
native-app-performance: A skill for development. This provides functionality for development workflows.
- native-app-performance
Native App Performance by the numbers
- 188 all-time installs (skills.sh)
- +6 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,091 of 4,348 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/steipete/agent-scripts --skill native-app-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 188 |
|---|---|
| repo stars | ★ 6.5k |
| Last updated | August 3, 2026 |
| Repository | steipete/agent-scripts ↗ |
What it does
Use native-app-performance for development tasks
Files
Native App Performance (CLI-only)
Goal: record Time Profiler via xctrace, extract samples, symbolicate, and propose hotspots without opening Instruments.
Quick start (CLI)
1) Record Time Profiler (attach):
# Start app yourself, then attach
xcrun xctrace record --template 'Time Profiler' --time-limit 90s --output /tmp/App.trace --attach <pid>2) Record Time Profiler (launch):
xcrun xctrace record --template 'Time Profiler' --time-limit 90s --output /tmp/App.trace --launch -- /path/App.app/Contents/MacOS/App3) Extract time samples:
scripts/extract_time_samples.py --trace /tmp/App.trace --output /tmp/time-sample.xml4) Get load address for symbolication:
# While app is running
vmmap <pid> | rg -m1 "__TEXT" -n5) Symbolicate + rank hotspots:
scripts/top_hotspots.py --samples /tmp/time-sample.xml \
--binary /path/App.app/Contents/MacOS/App \
--load-address 0x100000000 --top 30Workflow notes
- Always confirm you’re profiling the correct binary (local build vs /Applications). Prefer direct binary path for
--launch. - Ensure you trigger the slow path during capture (menu open/close, refresh, etc.).
- If stacks are empty, capture longer or avoid idle sections.
xcrun xctrace help recordandxcrun xctrace help exportshow correct flags.
Included scripts
scripts/record_time_profiler.sh: record via attach or launch.scripts/extract_time_samples.py: export time-sample XML from a trace.scripts/top_hotspots.py: symbolicate and rank top app frames.
Gotchas
- ASLR means you must use the runtime
__TEXTload address fromvmmap. - If using a new build, update the
--binarypath; symbols must match the trace. - CLI-only flow: no need to open Instruments if stacks are symbolicated via
atos.
#!/usr/bin/env python3
import argparse
import subprocess
from pathlib import Path
def main() -> int:
parser = argparse.ArgumentParser(description="Export time-sample XML from a .trace file.")
parser.add_argument("--trace", required=True, help="Path to .trace bundle")
parser.add_argument("--output", required=True, help="Output XML path")
args = parser.parse_args()
trace = Path(args.trace)
output = Path(args.output)
if not trace.exists():
raise SystemExit(f"trace not found: {trace}")
# xctrace export needs an XPath into the trace table-of-contents. The schema
# name 'time-sample' is stable for Time Profiler sample tables.
xpath = '/trace-toc/run[@number="1"]/data/table[@schema="time-sample"]'
cmd = [
"xcrun",
"xctrace",
"export",
"--input",
str(trace),
"--xpath",
xpath,
"--output",
str(output),
]
subprocess.check_call(cmd)
if not output.exists():
raise SystemExit(f"export failed: {output} missing")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env bash
set -euo pipefail
# Minimal CLI wrapper for Time Profiler recording via xctrace.
# Supports attach or launch and always writes a .trace output.
usage() {
cat <<'USAGE'
Usage:
record_time_profiler.sh --attach <pid> --trace <path> [--duration 90s]
record_time_profiler.sh --launch <binary> --trace <path> [--duration 90s]
USAGE
}
attach_pid=""
launch_cmd=""
trace_path=""
duration="90s"
while [[ $# -gt 0 ]]; do
case "$1" in
--attach)
attach_pid="$2"; shift 2 ;;
--launch)
launch_cmd="$2"; shift 2 ;;
--trace)
trace_path="$2"; shift 2 ;;
--duration)
duration="$2"; shift 2 ;;
-h|--help)
usage; exit 0 ;;
*)
echo "Unknown arg: $1"; usage; exit 1 ;;
esac
done
if [[ -z "$trace_path" ]]; then
echo "--trace is required"; usage; exit 1
fi
if [[ -n "$attach_pid" && -n "$launch_cmd" ]]; then
echo "Use either --attach or --launch, not both"; exit 1
fi
if [[ -z "$attach_pid" && -z "$launch_cmd" ]]; then
echo "Must supply --attach or --launch"; usage; exit 1
fi
if [[ -n "$attach_pid" ]]; then
xcrun xctrace record --template 'Time Profiler' --time-limit "$duration" \
--output "$trace_path" --attach "$attach_pid"
else
xcrun xctrace record --template 'Time Profiler' --time-limit "$duration" \
--output "$trace_path" --launch -- "$launch_cmd"
fi
#!/usr/bin/env python3
import argparse
import subprocess
import xml.etree.ElementTree as ET
from collections import Counter
from pathlib import Path
from typing import Dict, List, Tuple
def parse_text_vmsize(binary: Path) -> int:
# otool -l provides __TEXT vmsize; we need it to cap the address range.
out = subprocess.check_output(["otool", "-l", str(binary)], text=True)
lines = out.splitlines()
in_text = False
for i, line in enumerate(lines):
if line.strip() == "segname __TEXT":
in_text = True
if in_text and line.strip().startswith("vmsize"):
_, size_hex = line.strip().split()
return int(size_hex, 16)
# Stop scanning once we leave the __TEXT section block
if in_text and line.strip().startswith("segname") and line.strip() != "segname __TEXT":
in_text = False
raise SystemExit("Could not find __TEXT vmsize via otool -l")
def load_callstacks(samples_xml: Path) -> List[int]:
root = ET.parse(samples_xml).getroot()
# kperf-bt entries are referenced by id/ref; build a map first.
bt_by_id: Dict[str, List[int]] = {}
for bt in root.findall('.//kperf-bt'):
bid = bt.get('id')
text_addrs = bt.find('text-addresses')
if bid and text_addrs is not None and text_addrs.text:
# Addresses are space-separated decimal strings.
addrs = [int(x) for x in text_addrs.text.strip().split() if x.strip().isdigit()]
bt_by_id[bid] = addrs
addrs: List[int] = []
for row in root.findall('.//row'):
bt = row.find('kperf-bt')
if bt is None:
continue
ref = bt.get('ref')
if ref and ref in bt_by_id:
addrs.extend(bt_by_id[ref])
else:
bid = bt.get('id')
if bid and bid in bt_by_id:
addrs.extend(bt_by_id[bid])
return addrs
def chunked(items: List[str], size: int) -> List[List[str]]:
return [items[i:i + size] for i in range(0, len(items), size)]
def symbolicate(binary: Path, load_addr: str, addrs: List[int]) -> List[str]:
# atos can take multiple addresses; chunk to avoid arg limits on large traces.
addr_hex = [hex(a) for a in addrs]
symbols: List[str] = []
for chunk in chunked(addr_hex, 80):
cmd = ["xcrun", "atos", "-o", str(binary), "-l", load_addr] + chunk
symbols.extend(subprocess.check_output(cmd, text=True).splitlines())
return symbols
def main() -> int:
parser = argparse.ArgumentParser(description="Rank top hotspots from Time Profiler samples.")
parser.add_argument("--samples", required=True, help="time-sample XML from extract_time_samples.py")
parser.add_argument("--binary", required=True, help="Path to app binary")
parser.add_argument("--load-address", required=True, help="Runtime __TEXT load address (from vmmap)")
parser.add_argument("--top", type=int, default=30, help="Top N symbols")
args = parser.parse_args()
samples_xml = Path(args.samples)
binary = Path(args.binary)
load_addr = args.load_address
if not samples_xml.exists():
raise SystemExit(f"samples not found: {samples_xml}")
if not binary.exists():
raise SystemExit(f"binary not found: {binary}")
vmsize = parse_text_vmsize(binary)
base = int(load_addr, 16)
end = base + vmsize
addrs = load_callstacks(samples_xml)
counts = Counter(addrs)
# Filter to app addresses only, using runtime load address + __TEXT size.
app_counts = Counter({a: c for a, c in counts.items() if base <= a <= end})
top = app_counts.most_common(args.top)
if not top:
print("No app frames found; check load address and binary match.")
return 0
addrs_only = [a for a, _ in top]
symbols = symbolicate(binary, load_addr, addrs_only)
print("address,count,symbol")
for (addr, count), symbol in zip(top, symbols):
print(f"{hex(addr)},{count},{symbol}")
return 0
if __name__ == "__main__":
raise SystemExit(main())