firebase / firebase/firebase-ios-sdk

Firebase `SessionsDependencies.addDependency` — pre-main launch cost

Open
#16,264 1 comment 0 reactions 0 assignees View on GitHub
api: crashlytics type: feature request
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.