Black-box runtime performance profiling of an installed app — everything here
works on a stock release APK, no root, no source, no debuggable/profileable
flag. scripts/profile.py (standard library) wraps the no-root-safe half of
the Android profiling toolkit and benchmarks results against published
thresholds.
For a quick one-shot memory + jank number, android-package-diagnostics perf
is enough; this skill is the deep, benchmarked version (startup medians, jank
percentiles, Perfetto capture, baseline-profile status).
When NOT to use
- Crashes / ANRs / a quick perf glance / permissions →
android-package-diagnostics. - What's inside the APK file (baseline.prof presence, size, native ABIs) →
android-apk-analysis(static; this skill measures the running app). - Network timing/hosts →
android-network-trace. UI driving →android-ui-driver. - Heap dumps, allocation tracking, Studio Profiler, simpleperf app profiling → need a profileable/debuggable app or root; out of scope (see references).
The emulator caveat — read first
An emulator has no real GPU, thermal, or power behavior. These numbers are good for finding regressions, gross problems, and relative comparisons, but they are not device-grade benchmarks. Confirm anything that matters on a physical device. Every command says so in its output.
Workflow
Run as python3 "${CLAUDE_SKILL_DIR}/scripts/profile.py" <cmd> <pkg>.
-
Startup —
am start -W, median of N, classified against Android vitals (cold excessive ≥5 s). It measures PROCESS-COLD only (force-stopped), reports every sample + the count over the excessive line, and says "fast/ acceptable/exceeds" rather than grading a median "good":profile.py startup com.example.app --runs 5 # process-cold: force-stops each run -
Frames / jank — reset, exercise, then read the janky % and frame-time percentiles (budget 16.6 ms @60 Hz; <1% janky is good):
profile.py frames com.example.app --reset # zero the counters # ... scroll and navigate the app (android-ui-driver swipe) ... profile.py frames com.example.app # janky %, 50/90/95/99th -
Memory — PSS breakdown (Java/Native heap PSS; app must be running); re-run to watch for a leak:
profile.py memory com.example.app -
Baseline profile / compilation —
speed-profilemeans startup is optimized:profile.py dexopt com.example.app -
Perfetto trace — a full system trace, no root, for deep analysis:
profile.py trace com.example.app --secs 15 --out startup.perfetto-trace # exercise the app during the window; then open the file at ui.perfetto.dev
Output spec
- Each metric with a plain verdict tied to a named threshold, not a vibe (startup: fast/acceptable/exceeds-excessive; frames: good/ok/poor).
--jsonon startup/frames/memory/dexopt for machine use.- The Perfetto trace as a
.perfetto-tracefile plus how to analyze it — this skill does not parse the protobuf; it hands it to the Perfetto UI / trace_processor. - The emulator caveat on any timing/frame number.
Gotchas
- Emulator numbers aren't device numbers (see above). Use them for regressions and gross problems; benchmark for real on hardware.
- "Cold" here is process-cold, not first-install-cold. The skill force-stops
before each run, but page cache, app data, and compilation persist — so it's a
TTID-like proxy (
am start -W TotalTime), not TTFD or field Vitals. It reports all samples, not just the median (a low median can hide slow outliers). - Frame stats need frames, and they're HWUI stats. A freshly launched app has
rendered a handful; the janky % over 7 frames is noise (
frames --reset, exercise, then read). They're HWUI render stats (may miss SurfaceView/Compose- to-Surface); the 16.6 ms budget assumes 60 Hz — on 90/120 Hz use a Perfetto FrameTimeline trace, not this. status=verifyonly means "not currently profile-guided AOT compiled" — it does NOT prove a profile is missing or queued (especially for sideloaded apps).speed-profilemeans profile-guided AOT is active, but the profile's source (shipped baseline / Play cloud / runtime) can't be told apart here. For a real baseline-profile signal, checkassets/dexopt/baseline.profin the APK withandroid-apk-analysis.- Perfetto is captured, not parsed here. The trace is a protobuf; read it in
ui.perfetto.dev(FrameTimeline shows per-frame jank attributed to the app vs SurfaceFlinger) or query it withtrace_processor_shell— see references. A non-empty trace isn't proof of useful data (sources can be denied), and it embeds package/activity/thread strings — treat it as sensitive. am start -Wneeds the right launcher activity. The skill auto-resolves it; pass--activity pkg/.Compif the app has an unusual entry point.- PSS vs RSS: the memory numbers are PSS categories from
dumpsys meminfo(Java/Native are PSS heap buckets, not allocated-heap or USS); PSS is the standard per-app RAM cost, RSS over-counts shared memory. Watch PSS growth across runs for a leak. - Deep profiling needs more than this — heap dumps (
am dumpheap), allocation tracking, and simpleperf app profiling require the app to shipprofileable/debuggableor a rooted image.references/deep-profiling.md.
Files
scripts/profile.py— startup, frames, memory, dexopt, trace,--self-test(offline: parses/classifiesam start -W, gfxinfo, meminfo, dexopt output — no device).references/deep-profiling.md— Perfetto trace analysis (trace_processor SQL), the profileable/debuggable/root matrix, and what each unlocks.