Skip to content

fix(android): harden startup paths that turned a slow datastore into a hang - #262

Merged
ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/261-startup-hardening
Sep 14, 2026
Merged

ErikBjare merged 3 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/261-startup-hardening

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Follow-ups from #261. The root cause (quadratic legacy-bucket merge wedging the datastore worker) is fixed in ActivityWatch/aw-server-rust#679; this PR hardens the app-side paths that turned a busy datastore into a blank screen and ANRs, so the next slow datastore operation degrades gracefully instead. Not required for v0.14.1, but cheap and independent.

  • BackgroundService: queue the hostname/legacy-bucket migrations once per process. In the emulator repro onStartCommand ran twice on one launch, so the blocking migration command was queued twice on the single datastore worker.
  • WebUIFragment: the onReceivedError retry did Thread.sleep(100) on the main thread and reloaded immediately, which turned a slow server start into a tight reload loop on the UI thread. Now a Handler retry with backoff (250 ms → 5 s), reset on a successful page load and cancelled in onDestroyView. Unit test for the backoff.
  • WebWatcher: bucket creation moved off the accessibility service's main thread (same pattern MediaWatcher already uses). Play vitals list "Executing service WebWatcher" ANRs parked in Datastore::get_buckets.

Still open after this (separate): SessionEventWatcher/UsageStatsWatcher construct RustInterface on whatever thread creates them, and the JNI getBuckets path unwrap()s when the worker is gone (noted on the aw-server-rust PR).

@greptile-apps

greptile-apps Bot commented Sep 14, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the prior WebView retry finding is resolved and no new actionable defects remain.

Summary

  • Queues startup migrations only once per service process.
  • Replaces main-thread WebView retries with bounded asynchronous backoff that ignores subresource failures and survives failed-page completion callbacks.
  • Initializes the browser accessibility watcher’s datastore connection off the service main thread.
  • Adds unit coverage for retry classification, reset behavior, and delay capping.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Android startup] --> B[BackgroundService]
    B --> C{Migrations already queued?}
    C -->|No| D[Queue migrations on IO dispatcher]
    C -->|Yes| E[Skip duplicate migration work]

    A --> F[WebUIFragment]
    F --> G[Load local dashboard]
    G -->|Main-frame failure| H[Schedule delayed retry]
    H --> I[Increase delay up to 5 seconds]
    G -->|Successful finish| J[Cancel retry and reset delay]
    G -->|Subresource failure| K[Keep current page]

    A --> L[WebWatcher]
    L --> M[Initialize RustInterface on worker thread]
    M -->|Ready| N[Record browser sessions]
    M -->|Not ready| O[Drop early events without blocking service]
Loading

Reviews (2) · Last reviewed commit: "fix(webui): keep startup retries through..."

Comment thread mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt Outdated
TimeToBuildBob added a commit to TimeToBuildBob/aw-android that referenced this pull request Sep 14, 2026
…s on load

Greptile on ActivityWatch#262: a failed subresource scheduled a full-page reload, and a
successful onPageFinished reset the backoff without cancelling it, so a
usable dashboard could keep reloading every 250 ms. Use the
WebResourceRequest overload, ignore non-main-frame failures, and drop the
pending retry once a page finishes loading.
…a hang

Follow-ups from aw-android#261 (the migration itself is fixed in
aw-server-rust#679):

- BackgroundService: queue the hostname/legacy-bucket migrations once per
  process. onStartCommand ran twice on a single launch in the repro, so the
  blocking migration command was queued twice on the datastore worker.
- WebUIFragment: replace the Thread.sleep(100) + immediate reload in
  onReceivedError with a Handler-based retry that backs off from 250 ms to
  5 s and is cancelled in onDestroyView. The old loop blocked the main
  thread and hammered the server while it was still starting.
- WebWatcher: create the bucket off the accessibility service's main
  thread, like MediaWatcher already does. Play vitals showed
  'Executing service WebWatcher' ANRs parked in Datastore::get_buckets.

Refs ActivityWatch#261

Git-Session-Id: f014c682-2061-5a22-bc8e-de14775b115a
…s on load

Greptile on ActivityWatch#262: a failed subresource scheduled a full-page reload, and a
successful onPageFinished reset the backoff without cancelling it, so a
usable dashboard could keep reloading every 250 ms. Use the
WebResourceRequest overload, ignore non-main-frame failures, and drop the
pending retry once a page finishes loading.

Git-Session-Id: f014c682-2061-5a22-bc8e-de14775b115a
WebView still calls onPageFinished after a failed main-frame load.
Cancelling the pending Handler retry there dropped the ActivityWatch#261 backoff.
Only a successful finish cancels; subresource errors still do not schedule.

Git-Session-Id: f014c682-2061-5a22-bc8e-de14775b115a
@TimeToBuildBob
TimeToBuildBob force-pushed the fix/261-startup-hardening branch from 5b319e6 to cc3c231 Compare September 14, 2026 08:28
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@ErikBjare
ErikBjare merged commit 29577ab into ActivityWatch:master Sep 14, 2026
8 checks passed
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.

2 participants