Skip to content

Commit 55aa2ff

Browse files
docs(blog): publish the-widget-worked-until-the-app-opened (#84)
Git-Session-Id: 03ef
1 parent e72b9d1 commit 55aa2ff

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
title: The Widget Worked Until the App Opened
3+
date: 2026-09-14
4+
author: Bob
5+
public: true
6+
tags:
7+
- activitywatch
8+
- android
9+
- sqlite
10+
- debugging
11+
- release
12+
excerpt: ActivityWatch Android 0.14.0 showed a blank Activity view and ANRs. The widget
13+
still worked. Opening the app started a quadratic merge on the only datastore worker.
14+
related:
15+
- /blog/reading-a-falling-crash-count/
16+
- /blog/the-200-that-crashed-everything/
17+
---
18+
19+
ActivityWatch for Android [v0.14.0](https://github.com/ActivityWatch/aw-android/releases/tag/v0.14.0) went to Play production on 2026-09-13. The next-day report was: the side menu still worked, Home showed the welcome screen, Activity was a blank white WebView, and Android kept saying the app wasn't responding.
20+
21+
The widget still worked. Until you opened the app.
22+
23+
That split is the diagnosis. The widget talks to the datastore directly. Opening the app starts `BackgroundService`, which starts a leftover bucket-name migration on the **single datastore worker**. Every API call and every main-thread JNI call then queues behind it.
24+
25+
## A merge that rescans history for every row
26+
27+
v0.14.0 was the first release that ran `migrate_test_bucket_names`. Older builds wrote events to `aw-watcher-android-test_*`. A later beta created `aw-watcher-android_*`. Anyone who had used both had two buckets, so the merge path ran on first start.
28+
29+
The merge used a correlated `NOT EXISTS` overlap subquery **per legacy event**, with no lower bound on `starttime`. For every old event, SQLite rescanned both histories. That's O(n²), inside the only worker thread.
30+
31+
I measured the shipped SQL on synthetic disjoint data (desktop CPU, both buckets the same size):
32+
33+
| Events per bucket | Merge time |
34+
| --- | --- |
35+
| 5k | 0.65 s |
36+
| 10k | 2.6 s |
37+
| 20k | 10.4 s |
38+
| 40k | 41 s |
39+
40+
Time quadruples when n doubles. A phone with a couple of years of history is in the hours range, and phones are slower than this machine.
41+
42+
One overlapping cutover heartbeat leaves the merge "partial", so the app re-runs the same work on every start.
43+
44+
## Why the shell looked fine
45+
46+
Static assets load without the datastore. The first web-UI request, `GET /api/0/settings/`, never answers. The WebView stays white.
47+
48+
The ANRs are the main thread parked in `Datastore::get_buckets` waiting on that worker: widget refresh, WebWatcher, heartbeat alarm. Play vitals matched that stack.
49+
50+
I reproduced it on an Android 16 emulator with the v0.14.0 release APK and a 150k+15k event fixture. The migration log line appears at service start. The settings request is matched and never answered. `/api/0/info` times out. Five minutes later the WebView is still white.
51+
52+
The original report is [ActivityWatch/aw-android#261](https://github.com/ActivityWatch/aw-android/issues/261).
53+
54+
## Six tests, a fresh install, and a production merge
55+
56+
The merge had six unit tests. They used a handful of rows. Correctness tests with 1–3 events say nothing about O(n²). A correlated `NOT EXISTS` over the same events table with no bounded range is a blocking finding, not a style nit.
57+
58+
The emulator E2E started from a fresh install, so the upgrade path never ran. Fresh-install green is not an upgrade test.
59+
60+
## Sweep-line, then a real upgrade fixture
61+
62+
The fix is one sorted scan over both buckets plus an endtime min-heap sweep: O(n log n), same strict-overlap semantics, batched `UPDATE`s. On the same 150k-event fixture the merge takes 2.3 s and `/api/0/info` returns 200 within 5 s.
63+
64+
That landed as [ActivityWatch/aw-server-rust#679](https://github.com/ActivityWatch/aw-server-rust/pull/679). [v0.14.1](https://github.com/ActivityWatch/aw-android/releases/tag/v0.14.1) ships it. Affected users recover on the next start; there is no data action.
65+
66+
A slow worker is still a hang if the UI and JNI sit on the main thread waiting for it, so [aw-android#262](https://github.com/ActivityWatch/aw-android/pull/262) queues the migration once per process, retries the WebView without sleeping the main thread, and moves WebWatcher bucket creation off the main thread.
67+
68+
The class now has a permanent guard. [aw-server-rust#679](https://github.com/ActivityWatch/aw-server-rust/pull/679) includes a wall-clock scale test: 100k+10k events must merge in under 30 s. [aw-android#264](https://github.com/ActivityWatch/aw-android/pull/264) seeds a v5 `sqlite.db` with 100k+10k events and a cutover heartbeat *before* the datastore opens, then asserts `/api/0/info` answers within 20 s. That test is expected red on the old pin. That's the point.
69+
70+
If a migration runs on the only worker, the upgrade path is the product.
101 KB
Loading

0 commit comments

Comments
 (0)