Skip to content

[Bug]: v0.14.0-beta startup blocked for ~1 minute with many downloaded files #1997

Description

@FelixMau

I have verified that the bug is not already awaiting release

Yes

What was the Problem?

Since updating to v0.14.0-beta the Android app takes about a minute to start when there are many downloaded files. The main thread is blocked during Activity.onCreate, so the system logs Activity pause timeout / Activity stop timeout and am start -W times out. Nothing is rendered until the block ends.

Measured on a library with 2851 downloaded files (12 GB, per-chapter mp3s in a SAF folder on primary:Audiobooks):

20:43:23.642  Capacitor: Starting BridgeActivity
20:43:23.792  DeviceManager: Device Manager Singleton invoked
20:43:33.936  GC freed 37MB
20:44:18.234  AbsLogger: load: AbsLogger plugin initialized
20:44:20.018  mounted: initializing first load

54.4 s in one synchronous block, ~59 s until the UI is usable, on every cold start.

Root cause. Two Capacitor plugins do heavy I/O in load(), which Capacitor calls synchronously on the main thread from BridgeActivity.onCreate. Both code paths come from the download rewrite in eb2483d0 (merged via #1937, first released in v0.14.0-beta).

The numbers below are from a debug build on a second device with 1557 downloaded files, where Capacitor's Registering plugin instance: lines allow attributing the block to individual plugins. Absolute values are higher than in a release build; the ratios are the point.

Phase v0.14.0-beta
AbsDownloader.load() 16.6 s
AbsDatabase.load() 58.1 s
total block 75.4 s

1. AbsDatabase.load() — one storage-provider round-trip per file. cleanLocalLibraryItems checks every downloaded file. Up to v0.13.0-beta that was File(localFile.absolutePath).exists(). v0.14.0-beta replaced it with LocalFile.exists(ctx), which for content:// urls opens a real file descriptor:

ctx.contentResolver.openFileDescriptor(Uri.parse(contentUrl), "r")?.use { true }

That is one ContentResolver/Binder round-trip per file. During the block com.android.externalstorage sits at ~19% CPU (0 ticks when the app is idle) while the app's main thread waits on binder. For 1557 files these checks alone take 53.8 s. The motivation for the change is sound — File.exists() is unreliable under scoped storage — but the per-file cost went from microseconds to tens of milliseconds, on the main thread, at every start.

2. AbsDownloader.load() — queue restore on the main thread. DownloadServiceHost.ensure() deserializes the download db and probes shared storage with DocumentFile.findFile() (which lists the whole directory per lookup). The persisted items are read twice: once in DownloadItemManager's constructor via IncompleteDownloadCleanup.cleanupExpired(), once in restoreQueue() — visible as two GC spikes (40 MB, 19 MB). The db read alone measured 1.5–3.4 s. While this runs, the main thread also cannot attach the bridge, because setEventEmitter/hasWork wait on the same lock as restoreQueue.

3. Pre-existing, not a regression. Once the storage access is fixed, the cleanup is dominated by cleanLocalLibraryItems scanning the whole local file list for every track, twice — quadratic in the number of files an item has. For a book with 370 chapter files that is ~274k comparisons and 4.2 s; across the library 15.0 s.

Steps to Reproduce the Issue

  1. Download a library with a few thousand files into a folder on shared storage (SAF), e.g. 10 books of per-chapter mp3s.
  2. Force-stop the app.
  3. Start it and time until the UI appears: adb shell am start -W -n com.audiobookshelf.app/.MainActivity.
  4. The app is unresponsive for roughly a minute; logcat shows the gap between Starting BridgeActivity and load: AbsLogger plugin initialized.

Happens offline as well, so the server is not involved. Scales with the number of downloaded files, not their size.

What was Expected?

The app starts in about 2 s, as it did in v0.13.0-beta with the same library.

Phone Model

Fairphone 4

Phone OS

Android 15

Audiobookshelf App Version

Android App - 0.14.0-beta

Installation Source

Other (List in "Additional Notes")

Additional Notes

Installed from the GitHub release apk.

A fix is ready and tested on the affected device; PR follows and links back to this issue. It moves both cleanups off the main thread, establishes folder access once per item instead of once per file (keeping the full per-document check for when access is not confirmed), and replaces the quadratic track matching with a set lookup. Cold start goes from 75.4 s to 0.3–0.8 s, the cleanup itself from ~69 s to 1.6 s.

While investigating I also hit PluginLoadException: Unable to load plugin instance for AbsDownloader on v0.14.0-beta: attachBridge calls startForegroundService when the restored queue has work, and when the app is not allowed to start a foreground service at that moment (e.g. launched while the screen is locked) the resulting ForegroundServiceStartNotAllowedException propagates out of load(), so Capacitor drops the entire download plugin for that app session. That would leave the download button doing nothing until the app is restarted, which sounds a lot like what is reported in #1970 — I have not confirmed they are the same, but the PR guards that call.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions