diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 03d8e71c..e4250c29 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -40,8 +40,7 @@ "./skills/web-artifacts-builder", "./skills/webapp-testing" ] - } - , + }, { "name": "claude-api", "description": "Claude API and SDK documentation skill for building LLM-powered applications", @@ -50,6 +49,15 @@ "skills": [ "./skills/claude-api" ] + }, + { + "name": "android-perfetto", + "description": "Android Perfetto trace capture and analysis for startup, jank, ANR, memory, CPU scheduling, and binder IPC investigations", + "source": "./", + "strict": false, + "skills": [ + "./skills/android-perfetto-skill" + ] } ] } diff --git a/skills/android-perfetto-skill/LICENSE.txt b/skills/android-perfetto-skill/LICENSE.txt new file mode 100644 index 00000000..e23e9dd0 --- /dev/null +++ b/skills/android-perfetto-skill/LICENSE.txt @@ -0,0 +1,176 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/skills/android-perfetto-skill/SKILL.md b/skills/android-perfetto-skill/SKILL.md new file mode 100644 index 00000000..5934cf26 --- /dev/null +++ b/skills/android-perfetto-skill/SKILL.md @@ -0,0 +1,449 @@ +--- +name: android-perfetto-skill +description: Use this skill when capturing or analyzing Android Perfetto traces for app startup, UI jank, freezes, memory, CPU scheduling, binder IPC, or ANR investigations. It guides trace capture, SQL-based analysis, run comparison, and scored reporting with conservative confidence handling. +license: LICENSE.txt +compatibility: Requires adb and trace_processor_shell. Designed for Claude Code, Codex, and other Agent Skills-compatible coding agents working in an Android project or on exported Perfetto traces. +metadata: + author: Shaurya Jaiswal + version: 1.0.0 +--- + +## Prerequisites + +- `adb` must be available for `capture` routes, and capture expects exactly one connected device unless the user explicitly chooses one. +- `trace_processor_shell` must be installed at `/opt/homebrew/bin/trace_processor_shell` or available via an equivalent path. +- Perfetto MCP is preferred when available, but this skill must work fully with `trace_processor_shell` fallback. +- The project should expose an Android application ID in `buildSrc/src/main/kotlin/Config.kt`, `app/build.gradle`, `app/build.gradle.kts`, or equivalent Gradle configuration. +- Prefer a release-like `profileable` variant for realistic perf runs: basic Perfetto tracing works on release builds too, while heap/deep inspection may still require `debuggable`. +- Existing and newly captured runs live under `.perf/runs/-/`. + +## Interaction Principles + +These govern every phase of this skill. + +- **Default to Autonomy:** Try to resolve targets, configurations, and commands on your own using standard Android tools. Only prompt the user if you are genuinely blocked (e.g., missing prerequisites, ambiguous targets, or failed auto-recovery). +- **Smart Fallbacks:** If an automated step fails (like launching an app), do not immediately stop. Explain the failure briefly to the user and ask them to manually perform the action (like opening the app) so you can continue the workflow. +- **Physical interactions are the exception:** For `capture ui`, `capture freeze`, and as a last resort for `startup`, asking the user to navigate the app or reproduce an issue is expected. Wait for their confirmation before capturing. +- **Keep the user informed:** Briefly state what phase you are starting and summarize the results when moving to the next phase. + +## Phase 0: Intent Message + +State the intended route before doing any substantial work. + +- For `capture startup`: say you will capture a cold-start trace and save a new run folder. +- For `capture ui`: say you will capture a UI-interaction trace and prompt the user during the capture window. +- For `capture freeze`: say you will capture a freeze/ANR trace and collect trace plus sidecar artifacts. +- For `analyze [path]`: say which trace or run folder you are analyzing. +- For `compare`: say which two runs you will compare. +- For `report`: say whether you will show the latest report or a trend summary. +- For `fix`: say you will read the latest report and map findings to code without auto-applying changes. +- For the default route: say you will run the startup pipeline `capture -> analyze -> report`. + +**Reporting Action**: Before this phase, tell the user exactly which route you are taking. After this phase, confirm the route and the primary input you will use next. + +## Phase 1: Subcommand Routing + +Preserve the CLI-style subcommands exactly as behavior, but execute them through the phase routing below. + +| Invocation | Route | +|----------|--------| +| `capture startup` | Phase 2 -> Phase 3 (`startup`) | +| `capture ui` | Phase 2 -> Phase 3 (`ui`) | +| `capture freeze` | Phase 2 -> Phase 3 (`freeze`) | +| `analyze [path]` | Phase 2 -> Phase 4 -> Phase 5 -> Phase 6 | +| `compare` | Phase 2 -> Phase 7 | +| `report` | Phase 2 -> Phase 9 | +| `fix` | Phase 2 -> Phase 8 | +| `default` (no argument) | Phase 2 -> Phase 3 (`startup`) -> Phase 4 -> Phase 5 -> Phase 6 -> Phase 9 | + +Routing rules: + +1. If the user provides a trace path or run folder, use it instead of guessing. +2. If `analyze`, `report`, or `fix` is called without a path, use the latest run folder in `.perf/runs/`. +3. `capture` routes stop after artifact collection unless the user explicitly asked for a full pipeline or used the default route. +4. `report` must remain a first-class route. Do not fold it into `analyze`. + +**Reporting Action**: Before this phase, tell the user which route and scenario you selected. After this phase, tell the user which later phases will run and which ones are intentionally skipped. + +## Phase 2: Discovery and Environment Validation + +### Application ID detection + +1. Check for Android project files (`buildSrc/src/main/kotlin/Config.kt`, `app/build.gradle.kts`, `app/build.gradle`, etc.). +2. **Directory Check:** If no Android project files are found, pause and tell the user: *"I don't see an Android codebase in this directory. Running analysis here might mean missing out on mapping performance findings back to your code. Should I proceed anyway, or do you want to change directories first?"* Wait for their decision. +3. Extract the base `applicationId` from the project files (if available). Check `buildTypes` and `productFlavors` for `applicationIdSuffix`. +4. Verify the installed package with `adb shell pm list packages` and match it to your findings. +5. Store both `${BASE_APP_ID}` (for code search) and `${RUNNING_APP_ID}` (for device commands). + +### Target confirmation gate + +Runs before any `capture` route. First of two mandatory user-input checkpoints. Keep it lightweight — only ask when you genuinely cannot decide. + +1. **Ambiguity check** — if multiple `applicationId` candidates or installed packages match, present them and wait for the user's choice. If exactly one candidate exists, proceed and echo the resolved `${RUNNING_APP_ID}` so the user can correct you. +2. **Installation check** — verify `${RUNNING_APP_ID}` is installed via `pm list packages`. If missing, tell the user which package was expected, suggest the fix (install the build, switch device, or pick a different variant), and wait for `ready`. +3. **Scenario check** — if the user did not specify a scenario and the default route is not in use, confirm `startup`, `ui`, or `freeze` before capture. +4. Only after app identity, installation, device, and scenario are resolved, proceed to Phase 3. + +### Device and tool validation + +Follow the Interaction Principles: run every check yourself, describe any gap in plain English, and wait for `ready`. + +1. Run `adb devices` and require exactly one connected device, unless the user explicitly chose one. + - Zero devices: tell the user to connect via USB with USB Debugging enabled (Settings → Developer Options), or start an emulator. Also confirm they are in the Android project root (needed for `fix` and app-id detection); if not, ask them to navigate there and reply `ready`. + - Multiple devices: list serials with model names and ask which to target. +2. Note whether the device is an emulator or physical hardware in capture metadata. +3. Confirm `adb` and `trace_processor_shell` are on PATH. If either is missing, tell the user what is missing and the one-line install or PATH fix, then wait for `ready` and re-check. +4. Create `.perf/runs/` if it does not exist. + +### Launch activity resolution + +For `capture startup`, resolve `${LAUNCH_ACTIVITY}` before Phase 3. For `capture ui` and `capture freeze`, resolve it too when possible so the skill can recover if the target app is not already open. + +Preferred resolution order: + +1. `adb shell cmd package resolve-activity --brief -c android.intent.category.LAUNCHER ${RUNNING_APP_ID} | tail -1` +2. If that fails, parse the manifest or Gradle/Android sources for the exported launcher activity. +3. If resolution is still ambiguous for `startup`, stop and tell the user in plain English that startup capture cannot proceed until a launchable activity is identified, and suggest the likely cause (no exported `LAUNCHER` intent filter, obfuscated manifest, or wrong variant). Wait for the user to reply `ready` after they confirm or adjust the target before retrying. +4. If resolution is still ambiguous for `ui` or `freeze`, note that recovery launch is unavailable and require the app to already be open on the device before capture begins. + +### MCP availability probe + +Before analysis, test MCP once with a trivial query such as `SELECT 1`. + +- If MCP fails, set `MCP_AVAILABLE = false`, log the error once, and use `trace_processor_shell` for all subsequent analysis steps. +- If MCP succeeds, set `MCP_AVAILABLE = true` and use MCP as primary with per-step fallback only for data-specific failures. +- Do not retry MCP on every step after a connectivity failure. + +### Input selection + +1. For `analyze [path]`, accept either a run folder or a direct trace path. +2. For `compare`, identify the two most recent run folders unless the user provided explicit inputs. +3. For `report` and `fix`, resolve the latest analyzed run folder and its `report.md`. + +**Reporting Action**: Before this phase, tell the user you are validating the package, tools, and inputs. After this phase, tell the user the resolved package name, selected device or trace, MCP/fallback mode, and run folder target. If you had to pause for user input (ambiguous variants, multiple matching packages, uninstalled target, missing device, or missing tool), state what you were waiting on and what the user confirmed once they replied `ready`. + +## Phase 3: Capture + +Keep scenario-specific capture commands in this file because they are short, context-critical, and easy to lose when buried in references. + +### Perfetto config + +Write the Perfetto config to `perfetto-config.pbtxt` in the run folder. The capture command is: + +`cat perfetto-config.pbtxt | adb shell perfetto --txt -c - -o /data/misc/perfetto-traces/trace.pb` + +`--txt` is required. If it is omitted, Perfetto treats the config as binary and fails with `invalid config, bailing out`. If the capture command exits non-zero or prints `invalid config` / `bailing out`, stop, report the error, and do not pull or analyze the trace. + +For `startup`, treat this as the underlying Perfetto command, not a blocking foreground step. Start it asynchronously so the app can be launched while recording is already active. Save enough context to verify the capture started and later completed cleanly. + +Use this config template and replace `${DURATION}` plus `${ATRACE_APPS}`: + +```text +buffers { size_kb: 65536 fill_policy: RING_BUFFER } +buffers { size_kb: 8192 fill_policy: RING_BUFFER } +duration_ms: ${DURATION} + +data_sources { config { name: "linux.ftrace" ftrace_config { + ftrace_events: "sched/sched_switch" + ftrace_events: "sched/sched_waking" + ftrace_events: "sched/sched_blocked_reason" + ftrace_events: "binder/binder_transaction" + ftrace_events: "binder/binder_transaction_received" + ftrace_events: "binder/binder_lock" + ftrace_events: "binder/binder_locked" + ftrace_events: "binder/binder_unlock" + atrace_categories: "gfx" + atrace_categories: "view" + atrace_categories: "wm" + atrace_categories: "am" + atrace_categories: "dalvik" + atrace_categories: "binder_driver" + atrace_apps: "${ATRACE_APPS}" +} } } + +data_sources { config { name: "linux.process_stats" + process_stats_config { scan_all_processes_on_start: true proc_stats_poll_ms: 1000 } +} } + +data_sources { config { name: "android.surfaceflinger.frametimeline" } } +data_sources { config { name: "android.packages_list" } } +data_sources { config { name: "android.anr" } } + +data_sources { config { name: "linux.sys_stats" + sys_stats_config { meminfo_period_ms: 1000 meminfo_counters: MEMINFO_MEM_AVAILABLE meminfo_counters: MEMINFO_SWAP_FREE vmstat_period_ms: 1000 } +} } +``` + +Use: + +- `atrace_apps: "*"` for `startup`. +- `atrace_apps: "${RUNNING_APP_ID}"` for `ui` and `freeze`. +- Duration `20000ms` by default unless the user requested otherwise. + +The config must include the data sources needed for startup, frame analysis, binder, process resolution, ANR detection, and memory tracking. Keep the authoritative query expectations aligned with [references/perfetto-queries.md](references/perfetto-queries.md). + +### Startup capture + +Startup capture is **normally fully automated**. The skill should launch the app itself in the standard case. Asking the user to tap the launcher usually defeats cold-start measurement and can corrupt the trace. + +1. Confirm the Phase 2 target confirmation gate has passed. If it has not, return to Phase 2. Do not start capture. +2. `adb shell am force-stop ${RUNNING_APP_ID}` to guarantee a cold start. +3. Start the Perfetto capture **asynchronously** so recording is live before launch begins. Verify the capture started successfully and is still running; if startup of the capture command fails, stop and report the error. Do not continue to launch or analysis. +4. Wait roughly 2 seconds so trace collection is already stable when launch begins. +5. Launch the app yourself with `adb shell am start -W -n ${RUNNING_APP_ID}/${LAUNCH_ACTIVITY}` and save the output to `am_start_w.txt`. +6. **Launch Fallback:** If automated launch fails (for example `Error`, `does not exist`, or `Permission Denial`), abort the current startup capture attempt and tell the user the exact failure. Then ask one focused recovery question such as: *"I couldn't launch `${RUNNING_APP_ID}` automatically. Do I need a different activity name, extra flags, a setup step, or should I target a different package?"* If they prefer manual launch, make it explicit that you must retry the startup capture from the beginning so the cold start is still recorded. +7. After launch or user recovery, verify again that the target app is running and, when possible, foregrounded. If verification still fails, stop and do not capture. +8. After the launch succeeds, let the asynchronous capture run to completion before collecting artifacts. +9. After the trace completes, save `adb shell dumpsys meminfo ${RUNNING_APP_ID}` to `meminfo.txt`. +10. Pull the trace and sidecar artifacts only after the capture completed cleanly. + +### UI interaction capture + +UI capture requires the user to perform a real interaction inside the capture window. This is the second of two mandatory user-input checkpoints. The trace must not start until the user confirms they are ready. + +1. Confirm the Phase 2 target confirmation gate has passed. +2. Verify whether the target app is already running and, when possible, foregrounded (`pidof`, `dumpsys activity`, or equivalent). +3. If it is not already running or foregrounded and `${LAUNCH_ACTIVITY}` is known, try launching it yourself with `adb shell am start -W -n ${RUNNING_APP_ID}/${LAUNCH_ACTIVITY}` and save the output. If launch fails, tell the user the exact failure and ask them to open the app and navigate to the target screen, then reply `ready`. Do not start Perfetto before that recovery step succeeds. +4. If `${LAUNCH_ACTIVITY}` is unavailable and the app is not already open, ask the user to open the app and navigate to the target screen, then reply `ready`. +5. After launch or user recovery, verify again that the target app is running and, when possible, foregrounded. If verification still fails, stop and do not capture. +6. Ask the user to confirm they are ready on the exact screen they want traced. Do not start Perfetto before they acknowledge. +7. Reset frame stats with `adb shell dumpsys gfxinfo ${RUNNING_APP_ID} reset`. +8. Start the Perfetto capture. Verify the command succeeded: zero exit code and no `invalid config` / `bailing out` in output. If capture failed, stop and report the error. Do not continue to analysis. +9. Tell the user to perform the target interaction now, during the capture window. +10. After capture completes, save `adb shell dumpsys gfxinfo ${RUNNING_APP_ID} framestats` to `gfxinfo.txt`. +11. After capture completes, save `adb shell dumpsys meminfo ${RUNNING_APP_ID}` to `meminfo.txt`. +12. Pull the trace and sidecar artifacts only after app-state verification and capture success. + +### Freeze and ANR capture + +Freeze capture also requires the user to be ready to reproduce the hang during the trace window. Do not start Perfetto until the user confirms. + +1. Confirm the Phase 2 target confirmation gate has passed. +2. Verify whether the target app is already running and, when possible, foregrounded. +3. If it is not already running or foregrounded and `${LAUNCH_ACTIVITY}` is known, try launching it yourself with `adb shell am start -W -n ${RUNNING_APP_ID}/${LAUNCH_ACTIVITY}` and save the output. If launch fails, tell the user the exact failure and ask them to open the app, get it into the pre-freeze state, then reply `ready`. +4. If `${LAUNCH_ACTIVITY}` is unavailable and the app is not already open, ask the user to open the app, get it into the pre-freeze state, then reply `ready`. +5. After launch or user recovery, verify again that the target app is running and, when possible, foregrounded. If verification still fails, stop and do not capture. +6. Ask the user to confirm they are ready to reproduce the freeze or ANR. Do not start Perfetto before they acknowledge. +7. Start the Perfetto capture. Verify the command succeeded: zero exit code and no `invalid config` / `bailing out` in output. If capture failed, stop and report the error. Do not continue to analysis. +8. Tell the user to reproduce the freeze or hang immediately. +9. After capture completes, save `adb logcat -d` to `logcat.txt`. +10. After capture completes, save `adb shell dumpsys meminfo ${RUNNING_APP_ID}` to `meminfo.txt`. +11. Check `/data/anr/` and pull ANR artifacts when present. +12. Pull the trace and sidecar artifacts only after app-state verification and capture success. + +### Artifact contract + +Capture into `.perf/runs/-/` using this shape: + +```text +trace.perfetto-trace # always +metadata.json # always +perfetto-config.pbtxt # always +meminfo.txt # always +am_start_w.txt # startup only +gfxinfo.txt # ui only +logcat.txt # freeze only +anr/ # freeze only, if artifacts exist +``` + +`metadata.json` must include timestamp, scenario, package, device model, Android version, SDK level, battery/thermal context, and important capture caveats such as USB power or throttling. + +**Reporting Action**: Before this phase, tell the user what scenario you are about to capture and whether they need to interact. After this phase, tell the user where artifacts were saved, which sidecars were collected, and any obvious capture caveats. + +## Phase 4: Analysis + +Read the exact query sections named below from [references/perfetto-queries.md](references/perfetto-queries.md) before issuing SQL. Keep orchestration and heuristics here; keep full SQL bodies in the reference file. + +### Analysis setup + +1. Resolve `${TARGET_UPID}` using the ordered cascade in [references/perfetto-queries.md#process-resolution](references/perfetto-queries.md#process-resolution): + - direct process-name match + - suffix match using the last two package segments + - thread-name match against `` for cold-start traces + - UID match through `package_list` +2. If all four resolution steps return empty, stop analysis and tell the user the app process was not found in the trace. +3. Check DEX extraction and verification using [references/perfetto-queries.md#dex-compilation-state](references/perfetto-queries.md#dex-compilation-state). If DEX work is significant, flag the run as not AOT-compiled, report DEX time separately, recommend Baseline Profiles, and do not attribute that inflation to app code. +4. Load required modules using [references/perfetto-queries.md#perfetto-modules](references/perfetto-queries.md#perfetto-modules). If a required table still does not exist after `INCLUDE`, report `data source not in trace`, not `no events found`. + +### Required analysis steps + +1. **Frame performance**: Use [references/perfetto-queries.md#frame-performance](references/perfetto-queries.md#frame-performance). + - If `actual_frame_timeline_slice` is empty, check expected frames and `gfxinfo.txt`. + - If both Perfetto frame tables are empty and `gfxinfo` also shows no useful frames, mark frame analysis as `Insufficient data`. + - If `gfxinfo` is healthy but Perfetto attribution is unavailable, use `gfxinfo` for frame-health scoring and lower causality confidence. + - Only compute raw and adjusted jank when 30 or more usable frames exist. + - Use adjusted jank for scoring. Do not compute adjusted jank from unrelated or system-attributed frame rows. +2. **Cold-start timeline**: For startup runs, use [references/perfetto-queries.md#startup-timeline](references/perfetto-queries.md#startup-timeline). + - Reconstruct the timeline chronologically with nesting. + - Include `bindApplication`, `makeApplication`, `app.onCreate`, `performCreate`, first `Choreographer#doFrame`, `reportFullyDrawn`, inflation work, content providers, and framework-specific markers when present. + - Do not double-count child slices inside a parent slice. +3. **Main-thread hotspots**: Use [references/perfetto-queries.md#main-thread-hotspots](references/perfetto-queries.md#main-thread-hotspots). + - Exclude obvious debug-only tooling and dev-build libraries when they are clearly unrelated to production behavior. +4. **ANR detection**: Use [references/perfetto-queries.md#anr-detection](references/perfetto-queries.md#anr-detection). + - If ANRs are found, investigate blocked main-thread state around the ANR timestamp. +5. **Memory RSS and classification**: Use [references/perfetto-queries.md#memory-rss](references/perfetto-queries.md#memory-rss). + - Parse `meminfo.txt` alongside RSS. + - Distinguish startup allocation, startup allocation still in progress, and likely leak. Do not call startup allocation a leak. +6. **GC pressure**: Use [references/perfetto-queries.md#gc-pressure](references/perfetto-queries.md#gc-pressure). + - Correlate long GC pauses with janky frames when timestamps line up. +7. **CPU per thread**: Use [references/perfetto-queries.md#cpu-per-thread](references/perfetto-queries.md#cpu-per-thread). +8. **Thread contention**: Use [references/perfetto-queries.md#thread-contention](references/perfetto-queries.md#thread-contention). + - Group contention by root-cause lock owner. Multiple events on the same lock are one finding with repeated occurrences. +9. **Binder IPC**: Use [references/perfetto-queries.md#binder-ipc](references/perfetto-queries.md#binder-ipc). + - Main-thread binder calls over 100ms are ANR risks. + - Background-thread calls to `keystore2` are often TLS or credential setup; note them without over-alarming unless corroborating evidence exists. +10. **RenderThread check**: Use [references/perfetto-queries.md#renderthread-check](references/perfetto-queries.md#renderthread-check). + - If RenderThread max work is lower than main-thread max work, the bottleneck is not GPU-bound. + - If RenderThread dominates, switch recommendations toward overdraw, heavy animation, or complex drawing work. + +### Extend beyond the checklist when evidence demands it + +- If a slice name stands out, investigate it instead of ignoring it. +- If the app is Compose, Flutter, React Native, Unity, WebView-heavy, or otherwise framework-driven, include the equivalent runtime markers that better explain the behavior. +- Missing data is a finding. If `reportFullyDrawn` is absent, or ANR tables were not captured, say so explicitly. +- What is not in the trace can matter as much as what is present. + +**Reporting Action**: Before this phase, tell the user which trace you are analyzing and that you are resolving the process plus running the standard analysis sequence. After this phase, tell the user the strongest findings, major missing-data caveats, and whether confidence is limited by frame attribution, DEX state, or missing data sources. + +## Phase 5: Validation and Confidence + +Before writing or presenting conclusions, validate capture quality: + +1. The app process was found in the trace. +2. The scenario was actually captured. + - Startup: `bindApplication` or equivalent startup markers are present. + - UI: usable frame telemetry exists from Perfetto or healthy `gfxinfo`. + - Freeze: a stall, ANR marker, or corroborating freeze evidence exists. +3. Trace duration is long enough for the scenario. +4. Required data sources were present, or their absence is explicitly called out. + +Use only these confidence states: + +- `Issue found` +- `Likely issue, low confidence` +- `No issue found` +- `Insufficient data` + +False-positive guards: + +- Do not compute jank percentages from fewer than 30 usable frames. +- Do not emit `0% jank` when app-attributed frame telemetry is unavailable. +- Do not treat missing tables as proof that nothing happened. +- Do not compare startup timings across runs with different DEX compilation states as direct regressions. +- Do not downgrade confidence silently. Explain why confidence is reduced. + +**Reporting Action**: Before this phase, tell the user you are validating the trace and assigning confidence conservatively. After this phase, tell the user whether the run is high confidence, low confidence, or insufficient, and list the gating checks that affected that outcome. + +## Phase 6: Final Report + +Write the report to `.perf/runs//report.md`. + +Use [references/reporting.md#report-structure](references/reporting.md#report-structure) for the report shape and required sections. + +Use [references/reporting.md#score-breakdown-guidance](references/reporting.md#score-breakdown-guidance) for score thresholds and denominator reduction rules. + +For startup runs, use [references/reporting.md#startup-timeline-guidance](references/reporting.md#startup-timeline-guidance) so the timeline is chronological, nested, and separated into build-level versus code-level cost. + +For CPU and contention sections, use [references/reporting.md#cpu-and-threading-guidance](references/reporting.md#cpu-and-threading-guidance). + +For finding vocabulary, use [references/reporting.md#confidence-states](references/reporting.md#confidence-states). + +Append one JSON line to `.perf/history.jsonl` using [references/reporting.md#history-append](references/reporting.md#history-append). + +Required report sections: + +- `Capture Quality` +- `Score Breakdown` +- `Cold Start Timeline` for startup runs +- `Frame Performance` +- `Memory` +- `GC Pressure` +- `CPU & Threading` +- `Binder IPC` +- `Additional Findings` +- `Recommendations` + +**Reporting Action**: Before this phase, tell the user you are writing the final report and history entry. After this phase, tell the user where `report.md` was written, the headline score, and the top recommendations. + +## Phase 7: Compare + +Use [references/reporting.md#compare-guidance](references/reporting.md#compare-guidance) to compare two runs. + +Comparison rules: + +1. Compare individual metrics, not just headline score. +2. Call out differences in device, Android version, thermal state, scenario, app version, and DEX compilation state before labeling anything a regression. +3. If startup DEX state differs, compare only non-DEX startup portions. +4. If either startup run has fewer than 30 usable frame rows, do not compare startup jank percentages. +5. Distinguish likely noise from a real performance change. + +**Reporting Action**: Before this phase, tell the user which two runs you are comparing and which compatibility checks matter most. After this phase, tell the user the key improvements, regressions, and any comparison caveats that weaken confidence. + +## Phase 8: Fix + +Read the latest report and map findings back to code without auto-applying changes. + +Use the report plus trace evidence to search for likely source locations. Resolve the latest run folder, report path, and `${BASE_APP_ID}` via Phase 2 before beginning code search. + +Use the report plus trace evidence to search for likely source locations: + +- Slice names and startup markers -> search for matching methods, SDK initialization, or lifecycle hooks. +- Contention root causes -> search for the owning class, lock, or singleton access path. +- Memory findings -> search for allocation-heavy code or retained objects. +- Binder findings -> search for call sites, networking setup, keystore access, or IPC wrappers. + +Preferred remediation patterns: + +- Heavy `Application.onCreate` -> defer non-critical initialization. +- `runBlocking` on hot paths -> replace with suspend-based flow. +- Duplicate network clients or repeated session setup -> share client and connection state. +- Startup analytics or SDK initialization -> defer until post-first-frame. +- RecyclerView or layout cost -> flatten hierarchy, reduce bind work, prefetch, or simplify drawing. +- High JIT or DEX pressure -> recommend Baseline Profiles, code shrinking, or smaller startup surface. +- `GlobalScope` and similar lifecycle leaks -> move to lifecycle-aware scopes. +- WebView or Chromium startup on main thread -> defer creation or preload more carefully. +- DI or service-locator contention -> reduce lock serialization and startup graph work. + +Do not auto-apply code changes from the `fix` route. Rank fixes by likely impact and confidence. + +**Reporting Action**: Before this phase, tell the user you are mapping the latest findings to code and will return recommendations only. After this phase, tell the user the highest-impact fix candidates, where they likely live, and why they rank highest. + +## Phase 9: Report View + +`report` is a read-and-summarize route, not a re-analysis route. + +Use [references/reporting.md#report-view-guidance](references/reporting.md#report-view-guidance) when showing the latest report or trend summary. + +1. If a latest `report.md` exists, summarize its score, scenario, top findings, and major caveats. +2. If `.perf/history.jsonl` has two or more entries, show the short trend and recent delta before drilling into the latest run. +3. If there is no report yet, tell the user that no analyzed run is available and point them to `analyze` or the default route. + +**Reporting Action**: Before this phase, tell the user whether you are opening the latest report or summarizing recent history. After this phase, tell the user the latest score, trend direction, and the top issues or caveats worth acting on next. + +## Mandatory Rules + +- You **MUST** keep the user informed before and after every significant phase. Never execute a multi-step route silently. +- You **MUST** preserve the `capture`, `analyze`, `compare`, `report`, `fix`, and default routes as distinct behaviors. +- You **MUST** read the exact reference section named for each analysis or reporting step before using it. +- You **MUST** test MCP once, fail fast on connectivity issues, and switch to durable fallback instead of retrying every step. +- You **MUST** keep scenario-specific capture flow in this file and the heavy SQL in [references/perfetto-queries.md](references/perfetto-queries.md). +- You **MUST** state when confidence is reduced and why. +- You **MUST** pass the Phase 2 target confirmation gate before any capture. Never guess a variant silently and never run analysis on an unverified target. +- For `capture startup`, attempt to launch the app yourself via `adb shell am start -W`. If it fails, you MUST ask the user to manually launch the app and wait for their confirmation rather than abandoning the trace. +- The Perfetto capture command must include `--txt`. If capture fails (non-zero exit, `invalid config`, or `bailing out` in output), stop immediately and report the error. Do not pull a pre-existing device trace and do not proceed to analysis on a failed capture. +- For `capture ui` and `capture freeze`, try to launch the app yourself when the target is not already open and `${LAUNCH_ACTIVITY}` is known. If that launch fails, ask for the user's help before proceeding to capture. +- For `capture ui` and `capture freeze`, wait for the user to confirm readiness before starting Perfetto. +- For `capture ui` and `capture freeze`, verify the target app is running and, when possible, foregrounded before starting Perfetto. If it is not, recover by launching when safe and unambiguous; otherwise stop instead of collecting a random trace. +- The two mandatory user-input checkpoints are: (1) Phase 2 target confirmation gate, and (2) readiness handshake before `ui` or `freeze` capture. Do not skip either. +- Do not call startup allocation a leak. +- Do not compute or headline jank from fewer than 30 usable frames. +- Do not emit `0% jank` when frame attribution is unavailable. +- Do not confuse `data source not in trace` with `no events found`. +- Do not attribute DEX extraction or verification time to app code. +- Do not compare startup timings across different DEX states as direct regressions. +- Do not double-count nested startup slices. +- Do not report contention event-by-event; group by root cause. +- Do not over-alarm on background binder calls to `keystore2` without corroborating evidence. +- Do not claim a GPU-bound bottleneck until RenderThread evidence supports it. +- Do not auto-apply code changes during the `fix` route. +- If something important is missing from the trace, that absence is itself a finding and should be reported explicitly. diff --git a/skills/android-perfetto-skill/references/perfetto-queries.md b/skills/android-perfetto-skill/references/perfetto-queries.md new file mode 100644 index 00000000..5193af95 --- /dev/null +++ b/skills/android-perfetto-skill/references/perfetto-queries.md @@ -0,0 +1,383 @@ +# Perfetto Queries Reference + +Use this file for the full SQL bodies referenced by `android-perfetto-skill/SKILL.md`. Keep execution order and interpretation rules in the skill, and use this file only for the concrete queries. + +## Process resolution + +Run these in order and stop at the first successful match. + +### Direct process-name match + +```sql +SELECT upid +FROM process +WHERE name = '${RUNNING_APP_ID}'; +``` + +### Suffix match + +```sql +SELECT upid +FROM process +WHERE name LIKE '%${APP_SUFFIX}%'; +``` + +`APP_SUFFIX` is usually the last two package segments, for example `app.debug`. + +### Thread-name match for cold-start traces + +```sql +SELECT DISTINCT t.upid +FROM thread t +JOIN process p ON t.upid = p.upid +WHERE t.name LIKE '%${APP_SUFFIX}%' + AND p.name = '' +LIMIT 1; +``` + +### UID match through package list + +```sql +SELECT uid +FROM package_list +WHERE package_name = '${RUNNING_APP_ID}'; +``` + +If a UID is returned, resolve the process with: + +```sql +SELECT upid +FROM process +WHERE uid = ${UID}; +``` + +## DEX compilation state + +```sql +SELECT s.name, + ROUND(SUM(s.dur) / 1e6, 2) AS total_ms, + COUNT(*) AS count +FROM slice s +JOIN thread_track tt ON s.track_id = tt.id +JOIN thread t ON tt.utid = t.utid +WHERE t.upid = ${TARGET_UPID} + AND ( + s.name LIKE 'Extract dex file%' + OR s.name LIKE 'Verify dex file%' + OR s.name LIKE 'VerifyClass%' + ) +GROUP BY s.name +ORDER BY total_ms DESC +LIMIT 10; +``` + +## Perfetto modules + +Run these before steps that depend on derived Perfetto tables: + +```sql +INCLUDE PERFETTO MODULE android.binder; +INCLUDE PERFETTO MODULE android.anrs; +``` + +If a table is still missing after `INCLUDE`, the corresponding data source was not captured. + +## Frame performance + +### App-attributed frame timeline + +```sql +SELECT jank_type, + COUNT(*) AS frame_count, + ROUND(AVG(dur) / 1e6, 2) AS avg_ms, + ROUND(MAX(dur) / 1e6, 2) AS max_ms +FROM actual_frame_timeline_slice +WHERE upid = ${TARGET_UPID} +GROUP BY jank_type +ORDER BY frame_count DESC, max_ms DESC; +``` + +### Worst app-attributed frames + +```sql +SELECT ROUND(ts / 1e9, 3) AS ts_s, + ROUND(dur / 1e6, 2) AS frame_ms, + jank_type, + present_type +FROM actual_frame_timeline_slice +WHERE upid = ${TARGET_UPID} +ORDER BY dur DESC +LIMIT 20; +``` + +### Expected-frame fallback + +```sql +SELECT COUNT(*) AS frame_count +FROM expected_frame_timeline_slice +WHERE upid = ${TARGET_UPID}; +``` + +### Adjusted-jank numerator + +```sql +SELECT COUNT(*) AS adjusted_janky_frames +FROM actual_frame_timeline_slice +WHERE upid = ${TARGET_UPID} + AND jank_type LIKE '%App Deadline Missed%'; +``` + +### Raw-jank numerator + +```sql +SELECT COUNT(*) AS raw_janky_frames +FROM actual_frame_timeline_slice +WHERE upid = ${TARGET_UPID} + AND jank_type != 'None'; +``` + +### Total usable frames + +```sql +SELECT COUNT(*) AS total_frames +FROM actual_frame_timeline_slice +WHERE upid = ${TARGET_UPID}; +``` + +## Startup timeline + +```sql +SELECT s.name, + ROUND(s.dur / 1e6, 2) AS duration_ms, + ROUND(s.ts / 1e9, 3) AS timestamp_s, + s.depth, + s.parent_id +FROM slice s +JOIN thread_track tt ON s.track_id = tt.id +JOIN thread t ON tt.utid = t.utid +WHERE t.upid = ${TARGET_UPID} + AND ( + s.name LIKE '%ActivityThreadMain%' + OR s.name LIKE '%bindApplication%' + OR s.name LIKE '%makeApplication%' + OR s.name LIKE '%app.onCreate%' + OR s.name LIKE '%activityStart%' + OR s.name LIKE '%performCreate%' + OR s.name LIKE '%activityResume%' + OR s.name LIKE '%Choreographer#doFrame%' + OR s.name LIKE '%reportFullyDrawn%' + OR s.name LIKE '%inflate%' + OR s.name LIKE '%traversal%' + OR s.name LIKE '%layout%' + OR s.name LIKE '%Verify dex file%' + OR s.name LIKE '%Extract dex file%' + OR s.name LIKE '%VerifyClass%' + OR s.name LIKE '%ContentProvider%' + ) +ORDER BY s.ts ASC, s.depth ASC; +``` + +## Main-thread hotspots + +```sql +SELECT s.name, + ROUND(SUM(s.dur) / 1e6, 2) AS total_ms, + ROUND(MAX(s.dur) / 1e6, 2) AS max_ms, + COUNT(*) AS occurrences +FROM slice s +JOIN thread_track tt ON s.track_id = tt.id +JOIN thread t ON tt.utid = t.utid +WHERE t.upid = ${TARGET_UPID} + AND t.is_main_thread = 1 + AND s.dur > 5000000 +GROUP BY s.name +ORDER BY total_ms DESC +LIMIT 25; +``` + +## ANR detection + +### ANR rows + +```sql +SELECT * +FROM android_anrs +WHERE upid = ${TARGET_UPID}; +``` + +### Main-thread prerequisite + +Resolve `${MAIN_THREAD_UTID}` with: + +```sql +SELECT utid +FROM thread +WHERE upid = ${TARGET_UPID} + AND is_main_thread = 1 +LIMIT 1; +``` + +### ANR-window prerequisite + +Derive `${WINDOW_START_TS}` and `${WINDOW_END_TS}` from the ANR row you are investigating. For example, start with the ANR timestamp and inspect a window around it: + +```sql +SELECT ts, + ts - 5000000000 AS window_start_ts, + ts + 5000000000 AS window_end_ts +FROM android_anrs +WHERE upid = ${TARGET_UPID} +ORDER BY ts DESC +LIMIT 1; +``` + +### Main-thread state around an ANR window + +```sql +SELECT ROUND(ts / 1e9, 3) AS ts_s, + ROUND(dur / 1e6, 2) AS dur_ms, + state, + blocked_function +FROM thread_state +WHERE utid = ${MAIN_THREAD_UTID} + AND ts BETWEEN ${WINDOW_START_TS} AND ${WINDOW_END_TS} +ORDER BY ts; +``` + +### Scheduler slices around an ANR window + +```sql +SELECT ROUND(ts / 1e9, 3) AS ts_s, + ROUND(dur / 1e6, 2) AS dur_ms, + cpu, + end_state +FROM sched_slice +WHERE utid = ${MAIN_THREAD_UTID} + AND ts BETWEEN ${WINDOW_START_TS} AND ${WINDOW_END_TS} +ORDER BY ts; +``` + +## Memory RSS + +### RSS time series + +```sql +SELECT ROUND(c.ts / 1e9, 3) AS timestamp_s, + ROUND(c.value / 1e6, 2) AS rss_mb +FROM counter c +JOIN process_counter_track ct ON c.track_id = ct.id +WHERE ct.upid = ${TARGET_UPID} + AND ct.name = 'mem.rss' +ORDER BY c.ts; +``` + +### Peak RSS + +```sql +SELECT ROUND(MAX(c.value) / 1e6, 2) AS peak_rss_mb +FROM counter c +JOIN process_counter_track ct ON c.track_id = ct.id +WHERE ct.upid = ${TARGET_UPID} + AND ct.name = 'mem.rss'; +``` + +## GC pressure + +```sql +SELECT s.name AS gc_type, + COUNT(*) AS count, + ROUND(SUM(s.dur) / 1e6, 2) AS total_ms, + ROUND(AVG(s.dur) / 1e6, 2) AS avg_ms, + ROUND(MAX(s.dur) / 1e6, 2) AS max_ms +FROM slice s +JOIN thread_track tt ON s.track_id = tt.id +JOIN thread t ON tt.utid = t.utid +WHERE t.upid = ${TARGET_UPID} + AND ( + s.name LIKE '%concurrent%GC%' + OR s.name LIKE '%Background%GC%' + OR s.name LIKE '%young%GC%' + OR s.name LIKE '%explicit%GC%' + OR s.name LIKE '%alloc%GC%' + ) +GROUP BY s.name +ORDER BY total_ms DESC; +``` + +## CPU per thread + +```sql +SELECT t.name AS thread_name, + ROUND(SUM(ss.dur) / 1e6, 2) AS runtime_ms, + COUNT(*) AS schedules, + ROUND(MAX(ss.dur) / 1e6, 2) AS max_slice_ms +FROM sched_slice ss +JOIN thread t ON ss.utid = t.utid +WHERE t.upid = ${TARGET_UPID} +GROUP BY t.name +ORDER BY runtime_ms DESC +LIMIT 20; +``` + +## Thread contention + +### Raw contention events + +```sql +SELECT s.name, + ROUND(s.dur / 1e6, 2) AS duration_ms, + t.name AS thread_name +FROM slice s +JOIN thread_track tt ON s.track_id = tt.id +JOIN thread t ON tt.utid = t.utid +WHERE t.upid = ${TARGET_UPID} + AND s.name LIKE '%monitor_contention%' +ORDER BY s.dur DESC +LIMIT 50; +``` + +### Grouped contention by root cause + +```sql +SELECT s.name AS root_cause, + COUNT(*) AS events, + ROUND(SUM(s.dur) / 1e6, 2) AS total_blocked_ms, + ROUND(MAX(s.dur) / 1e6, 2) AS max_blocked_ms +FROM slice s +JOIN thread_track tt ON s.track_id = tt.id +JOIN thread t ON tt.utid = t.utid +WHERE t.upid = ${TARGET_UPID} + AND s.name LIKE '%monitor_contention%' +GROUP BY s.name +ORDER BY total_blocked_ms DESC +LIMIT 20; +``` + +## Binder IPC + +```sql +SELECT client_process, + server_process, + ROUND(client_dur / 1e6, 2) AS duration_ms, + client_thread +FROM android_binder_txns +WHERE client_upid = ${TARGET_UPID} + AND client_dur > 5000000 +ORDER BY client_dur DESC +LIMIT 15; +``` + +## RenderThread check + +```sql +SELECT s.name, + ROUND(s.dur / 1e6, 2) AS duration_ms +FROM slice s +JOIN thread_track tt ON s.track_id = tt.id +JOIN thread t ON tt.utid = t.utid +WHERE t.upid = ${TARGET_UPID} + AND t.name = 'RenderThread' + AND s.dur > 1000000 +ORDER BY s.dur DESC +LIMIT 15; +``` diff --git a/skills/android-perfetto-skill/references/reporting.md b/skills/android-perfetto-skill/references/reporting.md new file mode 100644 index 00000000..776b276b --- /dev/null +++ b/skills/android-perfetto-skill/references/reporting.md @@ -0,0 +1,142 @@ +# Reporting Reference + +Use this file when writing `report.md` or `compare` route output. + +## Report structure + +Write to `.perf/runs//report.md` using this shape: + +```markdown +# Performance Report — {package} + +**Date:** {timestamp} +**Scenario:** {startup|ui|freeze} +**Device:** {model} — Android {version} +**Score: {total}/{max}** + +## Capture Quality +{USB powered, thermal state, missing data sources, or other caveats} + +## Score Breakdown +| Category | Score | Rating | Confidence | +|----------|-------|--------|------------| + +## Cold Start Timeline (startup scenario) +| Event | Duration (ms) | Notes | +|-------|--------------|-------| + +## Frame Performance +{adjusted jank rate or gfxinfo-backed frame health, worst frames, jank type breakdown} + +## Memory +{RSS growth pattern, classification, meminfo breakdown if available} + +## GC Pressure +{event count, total duration, correlation with jank} + +## CPU & Threading +| Root Cause | Events | Total Blocked (ms) | Max (ms) | Waiters | Affected Threads | +|-----------|--------|-------------------|---------|---------|-----------------| + +## Binder IPC +{slow calls, which threads, which servers} + +## Additional Findings +{anything discovered beyond the default checklist} + +## Recommendations +{ranked by likely impact, tied to evidence, explicit about confidence} +``` + +## Startup timeline guidance + +- Lead with `am start -W` `TotalTime`. +- Present startup in chronological order with nesting. +- Separate **build-level** costs (DEX, JIT, class verification) from **code-level** costs (SDK init, DI, view inflation). +- Call out `bindApplication`, `performCreate`, major sub-costs, and whether `reportFullyDrawn` is absent. +- Do not double-count nested slices. + +Example rows: + +| Event | Duration (ms) | Notes | +|-------|--------------|-------| +| **am start -W TotalTime** | **1352** | **COLD** | +| `bindApplication` | **331.1** | Application startup | +| — DEX loading | 33.9 | Build-level | +| — SDK init | 130.8 | Defer if non-critical | +| `performCreate:SingleActivity` | **515.9** | Activity creation | +| First `Choreographer#doFrame` | — | TTFF marker | + +## Score breakdown guidance + +Use this rubric for `Score Breakdown`: + +| Category | Max | EXCELLENT | GOOD | ACCEPTABLE | POOR | +|----------|-----|-----------|------|------------|------| +| Frame Performance | 30 | <1% adjusted jank = 30 | 1-5% = 24 | 5-10% = 15 | >10% = 5 | +| ANR Stability | 25 | 0 ANRs = 25 | - | 1 ANR = 10 | 2+ = 5 | +| Memory Health | 20 | Stabilized <400MB = 20 | Stabilized 400-700MB = 14 | >700MB or unclear = 8 | Likely leak = 0 | +| CPU/Threading | 15 | No contention >50ms = 15 | Minor = 10 | Main blocked >200ms = 3 | - | +| Binder IPC | 10 | No main-thread >100ms = 10 | Minor = 7 | ANR-risk calls = 2 | - | + +If a category cannot be scored because data is missing, note it and reduce the denominator instead of guessing. + +Startup-specific scoring caveats: + +- If fewer than 30 frame rows are present, do not score `Frame Performance`. +- Do not use startup jank percentage as the headline comparison metric across devices. +- Prefer startup timeline phases, `am start -W`, DEX state, JIT pressure, and memory context. + +## CPU and threading guidance + +- Present top threads by CPU time. +- Group `monitor_contention` events by lock owner `class.method`. +- Rank contention by total blocked time, not max single event. +- Note `waiters=N` when useful. +- Classify each root cause as app, SDK, or system/framework when possible. + +Example contention rows: + +| Root Cause | Events | Total Blocked (ms) | Max (ms) | Waiters | Affected Threads | +|-----------|--------|-------------------|---------|---------|-----------------| +| **SdkHeartbeat.register()** | 7 | **471.9** | 151.0 | up to 3 | SDK background threads | +| NetworkClient.newStream() | 3 | 80.5 | 32.4 | 1 | Networking threads | +| DaggerSingletonC.get() | 1 | 29.4 | 29.4 | 0 | DefaultDispatcher | + +## Confidence states + +Use one of these for every finding: + +- **Issue found** — evidence is clear +- **Likely issue, low confidence** — evidence suggests a problem but is not conclusive +- **No issue found** — checked and looks fine +- **Insufficient data** — trace too short, scenario not captured, or data source missing + +## History append + +Append one JSON line to `.perf/history.jsonl`: + +```json +{"timestamp":"...","scenario":"...","score":76,"frames":24,"anr":25,"memory":14,"cpu":8,"binder":7,"adjusted_jank_pct":4.54,"raw_jank_pct":12.16,"rss_peak_mb":571,"gc_events":14,"gc_total_ms":906} +``` + +## Compare guidance + +For the `compare` route: + +1. List what changed: device, app version, scenario, thermal state, DEX state. +2. Compare key metrics side by side: startup time, adjusted jank or gfxinfo frame health, RSS peak, GC count, top contention. +3. Flag regressions and improvements with magnitude. +4. Say when a difference is likely noise rather than a real change. + +Important caveats: + +- If DEX extraction/verification differs between runs, startup timing is not directly comparable. Compare only the non-DEX portions. +- If either startup run has fewer than 30 frame rows, do not compare startup jank percentages. +- Cross-device comparisons should focus more on relative proportions than absolute timings. + +## Report view guidance + +- If a report exists, summarize the latest score, scenario, top findings, and top recommendations first. +- If history has two or more entries, show the recent trend and delta before drilling into the latest run. +- If no report exists yet, say that no analyzed run is available and point the user to `analyze` or the default route.