Skip to content

Hotfix/jrc 8103.bump.app.start.runnable.delay#8195

Closed
jrodiz wants to merge 2 commits into
firebase:mainfrom
jrodiz:hotfix/jrc--8103.Bump.app.start.runnable.delay
Closed

Hotfix/jrc 8103.bump.app.start.runnable.delay#8195
jrodiz wants to merge 2 commits into
firebase:mainfrom
jrodiz:hotfix/jrc--8103.Bump.app.start.runnable.delay

Conversation

@jrodiz
Copy link
Copy Markdown
Contributor

@jrodiz jrodiz commented May 21, 2026

firebase-perf: shadow-capture process start cause (#8103 follow-up, PR 1)

AppStartTrace's current "was this process forked to satisfy a foreground activity
launch?" heuristic relies on the relative ordering of a posted main-thread runnable vs
the first Activity.onCreate() Binder transaction. That ordering is undocumented,
already shifted once on API 34+ (originally fixed in PR #7281 / #5920, threshold-tightened
into a regression that #8103 reports, and shipping-patched to 1000 ms on the parent
commit of this PR), and could shift again.

This PR adds an OS-provided causal signal alongside the existing decision and emits both
as custom attributes on the existing _experiment_app_start_ttid trace, so we can
compare them in production telemetry before deciding (in a later PR) whether to flip the
authoritative classification.

Background

We ran a standalone experiment app
(api34-appstart-cause/) (TODO: UPLOAD TEST APP) on emulators at API 33, 34, 35, and 36
to validate the causal-signal approach before introducing it to the SDK. The full
results are out of tree but the headline findings are:

API Race reproduces on emulator? ApplicationStartInfo available? importance == FOREGROUND on launcher tap?
33 No No (API 35+ feature) Yes
34 No (matches PhilGlass's 2024 note from b/339891952) No Yes
35 Yes — 89 ms gap Yes — reason=LAUNCHER from first checkpoint Yes
36 Yes — 86 ms gap Yes — reason=LAUNCHER from first checkpoint Yes

So on API 35+, ApplicationStartInfo.getReason() is authoritative and available at the
first ContentProvider checkpoint. On API 34, RunningAppProcessInfo.importance at first
capture is a reliable foreground signal even when the timing-race itself doesn't
reproduce. Pre-API-34, the existing logic is correct and we don't try.

importanceReasonCode was tested and is not useful — always UNKNOWN (0) across
all runs.

Implementation

New file: ProcessStartCause.java

OS-version-conditioned helper. One static factory capture(@Nullable Context), immutable
fields, no held references:

ProcessStartCause cause = ProcessStartCause.capture(appContext);
cause.cause;            // FOREGROUND / BACKGROUND / UNKNOWN
cause.reasonName;       // "LAUNCHER" / "BROADCAST" / ... on API 35+, "" otherwise
cause.startTypeName;    // "COLD" / "WARM" / "HOT" on API 35+, "" otherwise
cause.importance;       // RunningAppProcessInfo.importance, or -1 if unread
cause.apiLevel;         // Build.VERSION.SDK_INT at capture
  • API 35+: ActivityManager.getHistoricalProcessStartReasons(1) via reflection. The
    repo's compileSdkVersion is 34, so ApplicationStartInfo can't be imported directly.
    Constants (START_REASON_* / START_TYPE_*) are redeclared as local int literals
    matching the public AOSP values; the helper logs the raw int as UNKNOWN_<n> if the
    values ever drift.
  • API 34: ActivityManager.getMyMemoryState(RunningAppProcessInfo).
    IMPORTANCE_FOREGROUNDFOREGROUND; anything else ⇒ UNKNOWN (we don't classify
    background here — caller falls back to the existing heuristic).
  • API < 34: Cause.UNKNOWN. The pre-API-34 timing logic is correct on those
    versions; the helper still captures importance for telemetry continuity.

AppStartTrace.java

  • One new field, captured once inside registerActivityLifecycleCallbacks (called from
    FirebasePerfEarly during the ContentProvider init chain — the experiment confirmed
    this is early enough that OS-set values reflect the original fork cause).
  • One new private method addProcessStartCauseExperimentAttributes(...) that puts six
    custom attributes on the existing _experiment_app_start_ttid trace, alongside the
    existing systemDeterminedForeground attribute:
Attribute Values
processStartCause foreground / background / unknown
processStartReasonApi35Plus e.g. LAUNCHER, BROADCAST, CONTENT_PROVIDER, or ""
processStartTypeApi35Plus COLD / WARM / HOT or ""
processImportanceApi34 raw int as string (only on API 34) or ""
timingWindowDecision foreground / background (mirrors the existing decision)
decisionAgreed true / false / unknown
  • Two new @VisibleForTesting accessors for the cause field.
  • Unchanged: MAX_BACKGROUND_RUNNABLE_DELAY, mainThreadRunnableTime,
    StartFromBackgroundRunnable, resolveIsStartedFromBackground(), and every
    isStartedFromBackground-gated early return in the lifecycle callbacks.

Tests

  • New ProcessStartCauseTest (7 cases) — covers null context, pre-API-34 path,
    API 34 with FOREGROUND / SERVICE / CACHED importance, constructor preservation,
    and the holder view. The API 35+ reflective branch is exercised end-to-end in the
    out-of-tree experiment app rather than under Robolectric 4.12, which doesn't support
    @Config(sdk = 35).
  • AppStartTraceTest gains experimentTrace_includesProcessStartCauseAttributes
    which injects a known ProcessStartCause and asserts all six attributes appear on the
    experiment trace with the expected values.

Rollout intent (post-merge)

  1. Wait for _experiment_app_start_ttid data to accumulate from the field. Compare
    processStartCause vs timingWindowDecision, grouped by processStartReasonApi35Plus
    and processImportanceApi34.
  2. Confirm the experiment's emulator-derived expectations hold on physical hardware,
    especially the processImportanceApi34 == 100 (FOREGROUND) → cold-launcher-start
    correspondence.
  3. PR 2 (separate PR): switch the actual classification to consult ProcessStartCause
    first, gated by a remote-config kill switch, with the timing window as fallback.
  4. PR 3 (separate PR): once PR 2 is stable in production, delete the timing-window
    machinery for API 34+ entirely.

This PR ships none of those next steps — it only adds the telemetry that informs PR 2.

Test plan

  • ./gradlew :firebase-perf:spotlessApply
  • ./gradlew :firebase-perf:testReleaseUnitTest — 12 test suites, 0 failures /
    0 errors. AppStartTraceTest: 9/9 (was 8/8). ProcessStartCauseTest: 7/7 (new).
  • Compile under compileSdkVersion = 34 confirmed (compileReleaseJavaWithJavac
    passes; the API 35+ reference is reflective so no missing-class warnings).
  • Manual: run the standalone experiment app at
    ~/AndroidStudioProjects/api34-appstart-cause/ against a physical API 34 Pixel
    device to confirm the importance signal matches the bug-reported environment.
    (Not gating this PR — PR 1 is observation-only and the emulator data already
    validates the signal collection.)

jrodiz added 2 commits May 20, 2026 01:21
…se#8103)

The API-34 fix in 22.0.2 (PR firebase#7281) used a 50 ms MAX_BACKGROUND_RUNNABLE_DELAY
to tell cold foreground starts from genuine background starts. That threshold
was calibrated against a near-empty test app on API 35, where the gap between
StartFromBackgroundRunnable firing and the first onActivityCreated was ~10 ms.

On API 34+ physical devices, the gap is dominated by Android's main-thread vs
Binder scheduling, not by app work:

  - 316 ms on a minimal ContentProvider repro (issue firebase#5920, PhilGlass).
  - 204 ms on a large production app (issue firebase#8103, manavchandrakar29011994).

50 ms is well below both, so _app_start is unconditionally suppressed for
typical real-world apps on the very devices the fix was meant to support.

Bump MAX_BACKGROUND_RUNNABLE_DELAY to 1000 ms. The mechanism (timing window)
and the warm-start guard introduced in PR firebase#7281 (mainThreadRunnableTime is
only set when onCreateTime == null) are unchanged. Genuine warm starts where
the process was forked for background work seconds-to-minutes before any
activity launch are still suppressed, since their gap far exceeds 1000 ms and
MAX_LATENCY_BEFORE_UI_INIT (60 s) remains the upper backstop.

Tests:
  - Rename the existing 50 ms tests to _within1000ms / _moreThan1000ms.
  - Add testStartFromBackground_largeAppGap_isForegroundStart: ~300 ms gap on
    API 34 now logs the trace (regression-locks firebase#8103).
  - Add testStartFromBackground_warmStart_stillSuppressed: 10 s gap is still
    suppressed.
…art detection

The timing-window heuristic in resolveIsStartedFromBackground() infers "was this
process forked to launch an Activity?" from the ordering of a posted runnable
vs the first onActivityCreated callback. That ordering already shifted once on
API 34+ (caused the original bug fixed in PR firebase#7281, and the threshold-too-tight
follow-up in firebase#8103) and could shift again on future OS versions.

Add a TODO documenting two cleaner alternatives so the next maintainer doesn't
have to re-derive them:

  - API 35+: ApplicationStartInfo / getHistoricalProcessStartReasons gives the
    OS's authoritative start reason (LAUNCHER, SERVICE, CONTENT_PROVIDER, ...).
    Removes the heuristic entirely on supported devices.

  - API 34: RunningAppProcessInfo.importanceReasonCode + .importance captured
    early in FirebasePerfEarly (before our own ContentProvider work mutates the
    cause), combined with the timing window as a fallback.

Comment-only change; no behavior or test surface affected.
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

@jrodiz jrodiz closed this May 22, 2026
@jrodiz jrodiz deleted the hotfix/jrc--8103.Bump.app.start.runnable.delay branch May 22, 2026 03:03
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.

1 participant