Repository navigation
Conversation
DirectApkSoSource opened a new ZipFile for every library whose dependencies it resolved, and once more per ABI path in prepare(). Opening a ZipFile parses the APK's whole central directory, so this cost grows with the number of entries in the APK, and apps paid it again for each native library they load at startup. Keep one ZipFile per APK that contains native libraries, for the lifetime of the source. APKs without native libraries, such as language or density splits, are closed at the end of prepare(), and the rest are closed when the source is replaced during recovery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A note on the bigger picture, since these two PRs only make the current path cheaper. I also measured the
As far as I can tell, React Native only needs SoLoader for the merged-library name mapping, but it always takes the top path: flowchart LR
RN["React Native<br/>SoLoader.init(context, mapping)"] --> F{"flags"}
F -->|"0, hard-coded today"| D["DirectApkSoSource"]
D --> S["Parse the APK's central directory<br/>#152: once instead of per library"]
S --> R["Read each library's ELF header<br/>for its dependencies<br/>#153: buffered reads"]
R --> L["dlopen"]
F -.->|"system loader flag"| W["System.loadLibrary<br/>with the mapped name"]
W --> L2["dlopen<br/>linker resolves dependencies"]
Since 8d9ffb1, passing an Fixing that properly would mean:
The trade-off is losing SoLoader's recovery. On this path the backup source is already disabled, so for React Native that mostly means no longer recovering when the APK moves while the app is running. I kept these PRs to changes with no behavior change, so they're easy to land on their own and help everyone on the current path. If the direction above sounds right, I'm happy to put together a PR for it. 🤖 Generated with Claude Code |
|
Same comparison on a more realistic app: the default Expo SDK 58 template with MMKV, Skia and SQLite added, which loads 23 libraries through
MMKV and SQLite both worked with the system loader flag: each wrote data at startup and read it back. 🤖 Generated with Claude Code |
|
Another data point in favour of this, from an Android TV app (react-native-tvos 0.86.0-2, SoLoader 0.12.1, Device: Android 14 set-top box, armeabi-v7a. APK: 114 MB, 2,505 entries. On Android 14 each uncached We noticed it when enabling R8 optimization (
Holding one |
DirectApkSoSourceopens a newZipFilefor every library whose dependencies it resolves, and once more for each ABI path inprepare(). Each open parses the APK's whole central directory, so the cost grows with the number of entries in the APK. In the React Native template that's 10 parses per cold start: 7 libraries plus 3 ABI paths. Related to #137.This keeps one
ZipFileopen per APK that has native libraries, for the lifetime of the source. APKs without libraries, like language or density splits, are closed at the end ofprepare().recover()closes the rest; it runs under the sources write lock, so nothing is reading them at that point.The cost is one file descriptor per APK plus its parsed central directory, about 37 KB for a 482-entry APK and 470 KB for a 5,482-entry one. One behavior change: if the APK moves while the app is running, dependency lookups keep reading the old file until recovery. The
dlopenthat follows still fails on the old path and triggers the same recovery as before.Test plan
Cold start of the React Native template release app (react-native 27e77216ef) on a OnePlus CPH2667 running Android 16, installed with its baseline profile. 4×4 Latin-square crossover, 640 launches per APK. The large APK is the same app with 5,000 small files added under
assets/.Median time to full display, from one run with all four builds side by side (R8 off, the template default):
mainAgainst
main, this PR is −2.4% (−5.9% to +1.0%) on the small APK and −14.0% (−16.2% to −11.6%) on the large one. Together with #153 it's −6.4% (−9.6% to −3.0%) and −14.8% (−17.2% to −12.3%). These are estimated over all launches with 95% confidence intervals.Repeating the run with R8 on gave the same picture: both PRs together measured −5.3% (−8.3% to −2.5%) and −13.4% (−16.3% to −10.7%) against
main.MinElfreturns the same dependencies as before for every library in both APKs, matchingllvm-readelf -d. I couldn't run the internalSoLoaderDirectApkTestsuite.🤖 Generated with Claude Code