Skip to content

Firebase SessionsDependencies.addDependency — pre-main launch cost #16264

Description

@canhth

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<Set<SessionsSubscriberName>>. 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:

+ (void)load {
  [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self withName:@"firebase-crashlytics"];
  [FIRSessionsDependencies addDependencyWithName:FIRSessionsSubscriberNameCrashlytics];  // runs pre-main
}

FirebaseSessions/Sources/Public/SessionsDependencies.swift:

public class SessionsDependencies: NSObject {
  private static let _dependencies =
    UnfairLock<Set<SessionsSubscriberName>>(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<Set<SessionsSubscriberName>> 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<Set<SessionsSubscriberName>>
 → swift_getTypeByMangledName
 → swift_checkGenericRequirements                        // Set<Element> 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

If using Swift Package Manager, the project's Package.resolved

Expand Package.resolved snippet
Replace this line with the contents of your Package.resolved.

If using CocoaPods, the project's Podfile.lock

Expand Podfile.lock snippet
Replace this line with the contents of your Podfile.lock!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions