Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Correctness and performance issues in watchers, sync and workers

オープン
#333 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 2 日以内に返信

@0xbrayo がすでに取り組んでいます。

2026年10月8日 から。

  • #339 @0xbrayo による — オープン
  • #342 @0xbrayo による — オープン
  • #350 @0xbrayo による — オープン

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
15/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
android, kotlin, rust
領域
mobile

調査の方向性

This is a tracking issue, not a single task: each checklist item names its own file and fix PR (#334 to #351), so pick one item and go to its named code, such as SessionEventWatcher's SimpleDateFormat parse or SyncScheduler. Start by reading that file and reproducing the described behaviour with its test or a log line. Done means that item's linked fix PR is merged and its checkbox is ticked; the Rust JNI item (#801) depends on an upstream change, so avoid it.

索引モデルが issue の本文から書いたものです。

説明

An audit of the app's watchers, sync, workers and widget found the correctness and performance issues below. Each item will be fixed in its own PR, linked next to it.

Correctness
  • Session watcher replays all history when the last event's timestamp has no fractional seconds. Fix: #334. SessionEventWatcher (and the legacy UsageStatsWatcher path) parse the last stored event's timestamp with SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"). aw-server-rust (chrono) omits the fraction when it is zero (2026-10-08T10:00:00Z), which throws ParseException. The watcher then treats the bucket as empty, queries usage events from 0, and every replayed session is inserted again because the heartbeat merge refuses events older than the last one. Roughly 1 in 1000 sessions starts on a whole second.

  • Unlock events are duplicated on every ingest run. Fix: #335. Unlocks are re-read from the start of the last session rather than from the unlock bucket's own last event. Every run re-sends all unlocks in that window; only the newest merges, the older ones are inserted again, so unlock counts inflate with each hourly alarm, app open and widget refresh.

  • Two datastores can be opened concurrently in the JNI layer. Fix: ActivityWatch/aw-server-rust#801 (needs an aw-server-rust submodule bump here once merged). openDatastore() in aw-server/src/android/mod.rs initialises an unsynchronised static mut DATASTORE. Several threads call it at startup (WebWatcher, MediaWatcher, SessionEventWatcher, the server thread, migrations), so two Datastore::new calls can race, giving two workers with separate SQLite connections and separate heartbeat caches.

  • Sessions of 4 hours or more are silently dropped. Fix: #336. SessionParser discards any session at or above MAX_REASONABLE_SESSION_DURATION (4h), losing genuine long sessions (navigation, video, reading). The cap exists to guard against missing PAUSE events, but the screen turning off is a better bound.

  • The in-progress foreground session is never counted until it ends. Fix: #337. The parser does not emit the trailing open session, so a 2-hour session in one app is invisible to the dashboard, widget and alerts until the user leaves it.

  • Category alerts are evaluated against stale data. Fix: #338. NotifyWorker queries category time without first ingesting new usage events, so alerts lag by up to the hourly (inexact) ingest alarm.

  • Cancelling a sync allows a second concurrent sync. Fix: #339. SyncInterface.cancel() clears the shared syncInFlight flag immediately, but interrupting the executor does not stop the native syncBoth, so a new sync can start while the old one is still running.

  • Sync runs twice per 15-minute interval. Fix: #340. SyncScheduler's Handler chain and the AlarmManager fallback both fire every 15 minutes; syncInFlight only prevents overlap, so each cycle can do two full syncs and two SAF mirrors. A stop/start while a sync is in flight can also leave two Handler chains running.

  • RustInterface.serverStarted is not volatile. Fix: #341. It is written on the main thread and polled in a loop by the hostname-rewrite thread, which may never observe the change.

  • Browser sessions are split by the keyboard and other overlay windows. Fix: #342. WebWatcher ends the session on any event from a different window id, so typing in the browser (IME window) or a popup ends and restarts the visit.

  • Media events are not merged when two players are active. Fix: #343. With two media sessions playing, heartbeats for different data alternate in one bucket, so they never merge and each 15s poll inserts new zero-duration events.

  • The app can crash with SIGBUS on first launch. BackgroundService rewrites bucket hostnames in sqlite.db through Android's SQLite while the Rust datastore, which bundles its own SQLite, may already have the same WAL database open (watchers open it over JNI before the server starts; the only guard is serverStarted). Each library resizes the shared -shm mapping under the other and the process dies with BUS_ADRERR in walIndexAppend. This is the intermittent Test release variant (R8 smoke) CI failure. Fix: #354.

Performance
  • WebWatcher writes events on the accessibility main thread. Fix: #344. logBrowserEvent calls the blocking JNI heartbeat on the service's main thread, the same pattern that caused the createBucket ANR in #261.
  • WebWatcher walks the accessibility tree on every content-change event. Fix: #345. Every browser typeWindowContentChanged event runs findWebView (up to 2000 binder calls), including for Firefox where it can never match.
  • BackgroundService.onCreate constructs RustInterface on the main thread. Fix: #346. loadLibrary + JNI initialise run on the main thread during service creation, which #262 moved off-thread everywhere else.
  • The SAF mirror rewrites every file on every sync. Fix: #347. mirrorDirectory copies all files in full regardless of whether they changed, so test.db is rewritten every 15 minutes.
  • Session ingest does redundant work per run. Fix: #348. One getApplicationInfo binder call per session (no cache), two full passes over queryEvents (sessions and unlocks), and createBucketHelper serialises every bucket on each of its two calls.
  • The widget runs the full-day query once per widget instance. Fix: #349. updateAllWidgets re-queries and re-reads settings for every placed widget instead of once.
  • NotifyWorker's preferences grow without bound. Fix: #350. A new triggered_<category>_<hash>_<date> key is written per alert per day and never removed; SharedPreferences loads the whole file on each access.
  • The share-sheet export fallback loads the whole export into memory on the UI thread. Fix: #351. shareExport calls readContent(), undoing the streaming from #304 on that path.
主要言語
Kotlin
スター
277
フォーク
61
平均マージ
3日 11時間
マージ済み PR(30日)
38

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

ActivityWatch/aw-android のほかの issue

ActivityWatch/aw-android の issue をすべて見る

似ている issue

Kotlin の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。