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
- Download a library with a few thousand files into a folder on shared storage (SAF), e.g. 10 books of per-chapter mp3s.
- Force-stop the app.
- Start it and time until the UI appears:
adb shell am start -W -n com.audiobookshelf.app/.MainActivity.
- 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.
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 logsActivity pause timeout/Activity stop timeoutandam start -Wtimes 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):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 fromBridgeActivity.onCreate. Both code paths come from the download rewrite ineb2483d0(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.AbsDownloader.load()AbsDatabase.load()1.
AbsDatabase.load()— one storage-provider round-trip per file.cleanLocalLibraryItemschecks every downloaded file. Up to v0.13.0-beta that wasFile(localFile.absolutePath).exists(). v0.14.0-beta replaced it withLocalFile.exists(ctx), which forcontent://urls opens a real file descriptor:That is one ContentResolver/Binder round-trip per file. During the block
com.android.externalstoragesits 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 withDocumentFile.findFile()(which lists the whole directory per lookup). The persisted items are read twice: once inDownloadItemManager's constructor viaIncompleteDownloadCleanup.cleanupExpired(), once inrestoreQueue()— 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, becausesetEventEmitter/hasWorkwait on the same lock asrestoreQueue.3. Pre-existing, not a regression. Once the storage access is fixed, the cleanup is dominated by
cleanLocalLibraryItemsscanning 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
adb shell am start -W -n com.audiobookshelf.app/.MainActivity.logcatshows the gap betweenStarting BridgeActivityandload: 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 instanceforAbsDownloaderon v0.14.0-beta:attachBridgecallsstartForegroundServicewhen 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 resultingForegroundServiceStartNotAllowedExceptionpropagates out ofload(), 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.