firebase / firebase/firebase-ios-sdk
Firebase `SessionsDependencies.addDependency` — pre-main launch cost
- Dominant language
- C++
- Stars
- 6.7k
- Forks
- 1.8k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 72
Description
### Description
### Description
We were chasing a cold-launch regression with Instruments and kept landing on `FIRSessionsDependencies.addDependency` in the pre-`main()` samples. After digging in, the call itself is fine — the problem is *where* it runs and what it drags in the first time it's touched.
`+[FIRCrashlytics load]` calls `[FIRSessionsDependencies addDependencyWithName:]` from a `+load`, so it fires during objc image init, before `main()`. That first call is also the first touch of the `_dependencies` static, which is a generic `UnfairLock>`. To instantiate that the Swift runtime has to build the generic metadata and prove `SessionsSubscriberName: Hashable` (because of `Set`). On a cold cache — and pre-main is as cold as it gets — that first conformance lookup walks the app's `__swift5_proto` records. In a big app that table is large, so the scan isn't free, and it's sitting right on the launch critical path.
So it's not the lock and not the insert. It's `swift_conformsToProtocol*` resolving the very first conformance query in the process, kicked off by a generic type that only gets built because something called into Sessions from `+load`.
### The code involved
`FirebaseCrashlytics/Crashlytics/Crashlytics/FIRCrashlytics.m`:
```objc
+ (void)load {
[FIRApp registerInternalLibrary:(Class)self withName:@"firebase-crashlytics"];
[FIRSessionsDependencies addDependencyWithName:FIRSessionsSubscriberNameCrashlytics]; // runs pre-main
}
```
`FirebaseSessions/Sources/Public/SessionsDependencies.swift`:
```swift
public class SessionsDependencies: NSObject {
private static let _dependencies =
UnfairLock>(Set()) // generic static, built on first touch
@objc public static func addDependency(name: SessionsSubscriberName) {
_dependencies.withLock { dependencies in
dependencies.insert(name)
}
}
}
```
Calling `addDependency` from `+load` is what forces that lazy `_dependencies` init to happen at image-init time instead of later.
### Why `+load` specifically
This one surprised us a bit, so worth spelling out: `+load` runs unconditionally at startup for static linkage too, not just dynamic. If you're linking Crashlytics statically there's no lazy-load escape hatch — the `+load` always runs pre-main. And it's independent of when the app calls `FirebaseApp.configure()`, since `+load` fires long before that. So configuring Firebase late in `didFinishLaunchingWithOptions:` doesn't move this off the launch path.
### What we'd suggest
The high-impact fix is to register the Crashlytics dependency somewhere other than `+load` — e.g. during component creation / `configure()` instead of image init. As far as we can tell the dependency set only needs to be populated before Sessions actually reads it, which still happens well after component setup, so moving it out of `+load` shouldn't change behavior but would take it off the pre-main path entirely.
If you wanted to also kill the generic-instantiation part, backing the set with a non-generic storage type would avoid the runtime having to reconstruct `UnfairLock>` on first touch. But honestly the `+load` move is the one that matters — that's what gets it off launch.
Happy to test a patch against our setup if that helps.
### One note to avoid confusion
This isn't the same thing as #15842 (the `RemoteSettings`/`SettingsCache` deadlock fix). That one doesn't touch `SessionsDependencies` and doesn't help here — we checked, the code on this path is unchanged through 12.10.0. Figured I'd call it out since it's the obvious "is this already fixed?" question.
Also worth saying: this scales with the host app's conformance count, so smaller apps probably never notice it. It only got loud for us because the table is big.
### Reproducing the issue
### Repro
Nothing exotic — link Crashlytics into an app that has a large Swift conformance table (lots of statically linked modules), profile a cold launch in Instruments (App Launch / Time Profiler), and look at the pre-`main()` region. You'll see something like:
```
start
→ dyld4 runAllInitializersForMain → runInitializersBottomUp → load_images
→ objc notifyObjCInit
→ +[FIRCrashlytics load]
→ +[FIRSessionsDependencies addDependencyWithName:]
→ one-time initialization function for SessionsDependencies._dependencies
→ __swift_instantiateConcreteTypeFromMangledName // build UnfairLock>
→ swift_getTypeByMangledName
→ swift_checkGenericRequirements // Set requires Element: Hashable
→ swift_conformsToProtocolMaybeInstantiateSuperclasses // this is where the time goes
```
The `swift_conformsToProtocol*` frame dominates the subtree once the cache is cold.
### Firebase SDK Version
12.8.0
### Xcode Version
26.0.1
### Installation Method
CocoaPods
### Firebase Product(s)
Crashlytics
### Targeted Platforms
iOS
### Relevant Log Output
```shell
```
### If using Swift Package Manager, the project's Package.resolved
Expand Package.resolved snippet
```json
Replace this line with the contents of your Package.resolved.
```
### If using CocoaPods, the project's Podfile.lock
Expand Podfile.lock snippet
```yml
Replace this line with the contents of your Podfile.lock!
```
Contributor guide
Assessment
This issue has not been assessed yet.