Skip to content

fix(android-profiler): explain an empty CPU section instead of rendering it as an idle app - #669

Open
filip131311 wants to merge 1 commit into
mainfrom
filip/android-no-callstacks-diagnostic
Open

fix(android-profiler): explain an empty CPU section instead of rendering it as an idle app#669
filip131311 wants to merge 1 commit into
mainfrom
filip/android-no-callstacks-diagnostic

Conversation

@filip131311

Copy link
Copy Markdown
Collaborator

Fixes #629.

Reproduced, then diagnosed harder than the report

Pixel_9 / API 36, com.android.settings, real interaction — all three symptoms reproduced:

native-profiler-analyze  → 70 hangs + RSS populated, NO CPU section,
                           "showing top 0 CPU hotspots", exportErrors: {}
thread_breakdown         → 296 samples across 9 threads
function_callers epoll_wait → not found

The report infers "unsymbolized frames". It's stronger than that — I queried the .pftrace directly:

value
samples for the process 296
samples with a callsite_id 0
stack_profile_frame rows (trace-wide) 0
stats.perf_samples_skipped 296
unwind_error NULL on every row

The unwinder never ran. There are no frames to symbolize. That's why thread_breakdown — which counts perf_sample rows and never joins the symbol tables — reports 296 while every stack-dependent view is empty.

Positive control: same emulator, debuggable app

adb shell getprop ro.build.type                      → user
dumpsys package com.android.settings | grep pkgFlags → [ SYSTEM HAS_CODE … ]   ← no DEBUGGABLE
dumpsys package com.anonymous.myapp  | grep pkgFlags → [ DEBUGGABLE HAS_CODE … ]
com.android.settings com.anonymous.myapp (debuggable)
samples / with callsite / named leaf 296 / 0 / 0 86 / 86 / 78
frames in trace 0 723
perf_samples_skipped 296 0
analyze no CPU section writel 48.72%, internal_get_user_pages_fast 6.41%

So this is not an emulator limitation and not symbolization — it's target profileability. The remedy the message gives is verified to work on the very device it describes.

The fix

The pipeline detects "samples exist, none carry a usable stack" and says so. It names the count and the package, and distinguishes two causes using leaf_mapping, already on the row — no new query needed. It comes from the same join chain as leaf_function, so a mapping with no name means the stack was unwound and only the symbol is missing (stripped .sos), while neither means nothing was unwound (not profileable). Different problems, different fixes.

Live output on the bad trace:

⚠️ CPU sampling: 296 CPU samples were captured for com.android.settings, but none carry a usable call stack, so no CPU hotspots could be computed and profiler-stack-query mode=function_callers / mode=hang_stacks will find nothing. The app was running — this is a capture limitation, not an idle app. mode=thread_breakdown still works, since it counts samples and needs no stacks. No stack was unwound at all: Perfetto only unwinds a process it can read /proc/<pid>/mem for. Profile a debug build, or add <profileable android:shell="true"/> to <application> and rebuild — verify with adb shell dumpsys package com.android.settings, which shows DEBUGGABLE for a profileable target.

Not routed through exportErrors. That looked free — it renders above the Summary with no plumbing — but renderAllClear counts its entries as failed queries and would print "1 of 3 queries errored" about three queries that all succeeded. It goes through a cpuDiagnostic note instead, alongside the existing freshnessNote, and status stays ok.

Also fixed: the footer no longer says "showing top 0 CPU hotspots" — the literal artifact in the report title.

What I deliberately did NOT touch

The existing no-samples-at-all manifest hint stays gated on hangRows.length === 0. That gate is intentional and pinned by manifest-hint.test.ts:52 ("does NOT emit a manifest hint when hangs were found but CPU was empty") — once hangs come back the app clearly was traced, so blaming the manifest is wrong advice.

It also wouldn't have helped: in the reported case cpuRows.length is 9, not 0, so state A is never reached. Ungating it would have fired the hint on ~6 more existing fixtures and flipped their status, for no benefit. I only corrected its text, which claimed "Perfetto silently drops the linux.perf data source" — this issue proves a non-profileable app still yields samples.

Verified live, both directions

trace CPU warning CPU section footer status
stackless (com.android.settings) ✅ shown absent showing top 3 hangs ok, exportErrors: {}
healthy (debuggable app) ✅ absent present showing top 2 CPU hotspots and top 3 hangs ok

Deliberately deferred (filed separately)

  • thread_breakdown / function_callers notes — the issue's "and ideally" half. Needs a new thread-breakdown.sql aggregate plus fixture updates in two test files.
  • A partially-stackless trace renders percentages that sum to 100% while describing only the surviving fraction — arguably more misleading than the empty case here, and a distinct defect.
  • Repo docs assert the behaviour this PR only now implements.

Checks

  • 3093 tests pass; 7 new. All 200 existing android-perfetto tests pass unmodified, including the manifest-hint gate.
  • tool-description-quality.yml doesn't run (path-filtered to src/tools/**); prettier, eslint, both typechecks clean; lock untouched.

…ing it as an idle app

A Perfetto trace can contain perf samples that carry no call stack. Every such
row is dropped for having no leaf function, so the report showed no CPU section
and the footer read 'showing top 0 CPU hotspots', while thread_breakdown — which
counts samples and never joins the symbol tables — reported hundreds. Two
statements about one trace that cannot both be true, with nothing explaining it.

The pipeline now detects that state and says so, naming the sample count and the
package. It is deliberately not an exportErrors entry: nothing errored, and the
all-clear banner counts those as failed queries, so routing it there would have
printed 'N of 3 queries errored' about queries that all succeeded.

Two causes are told apart with data already on the row. leaf_mapping comes from
the same join chain as leaf_function, so a mapping with no name means the stack
was unwound and only the symbol is missing (stripped libraries), while neither
means nothing was ever unwound (the process was not profileable). They need
different fixes, so guessing between them would send people down the wrong path.

Measured on a Pixel_9 while diagnosing this: com.android.settings, a platform
package with no DEBUGGABLE flag, yielded 296 samples with zero callsites and an
empty stack_profile_frame, and perf_samples_skipped equal to the sample count.
A debug build on the same emulator yielded 86 samples, 86 callsites, 723 frames
and a populated hotspot table — so the advice the message gives is verified on
the device it describes, and this is not an emulator limitation.

The existing no-samples-at-all hint is left gated on there being no hangs. That
gate is deliberate and tested: once hangs come back the app clearly was traced,
so blaming the manifest would be wrong. This is a different state and carries no
such gate, which matters because a real capture almost always has hangs.

The footer no longer claims to show 'top 0 CPU hotspots' when there are none.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android native profiler: analyze reports 0 CPU hotspots although the trace contains 390 samples, and no symbol resolves in function_callers

1 participant