Firefox macOS startup font initialization lock contention

Profile: https://profiler.firefox.com/public/fx5sg9g8p9bp6bq36aggg74r8cmrbfcaxcm1rb0

Firefox 142, macOS 15.5.0. The profile covers the first 1.59 seconds of startup.

What I was looking at

Three threads immediately stand out in profiler-cli profile info:

  • GeckoMain (t-0): 815ms active CPU

  • RegisterFonts (t-1): 121ms active CPU

  • InitFontList (t-5): 87ms active CPU

The two font threads are running concurrently with the main thread through much of the first 400ms. The question is whether they’re just doing independent work in parallel, or whether they’re in each other’s way.

Following the lock

I searched the main thread for psynch (the macOS kernel call for mutex waits) and found 112 samples containing it, all of them in the first 400ms. That window has 394 total samples, so the main thread is spending about 28% of the first 400ms blocked on a mutex.

The call chain is specific. The main thread is in NS_InitXPCOM, initializing the layout subsystem:

NS_InitXPCOM
  nsComponentManagerImpl::Init
    nsLayoutModuleInitialize
      nsLayoutStatics::Initialize
        nsContentUtils::Init
          nsXPLookAndFeel::GetInstance
            nsLookAndFeel::EnsureInit
              [NSWindow initWithContentRect:...]
                [NSThemeFrame _updateTitleProperties:...]
                  [NSFrameView titleCell]
                    [NSTextFieldCell initTextCell:]
                      [NSCell setStringValue:]
                        UIFoundation!+[NSFont systemFontOfSize:width:]
                          CoreText!TDescriptor::CreateMatchingDescriptorInternal
                            CoreText!MakeSpliceDescriptor
                              libFontRegistry.dylib!XTCopyFontWithName
                                libFontRegistry.dylib!TLocalFontRegistry::TLocalFontRegistry()
                                  _pthread_mutex_firstfit_lock_slow
                                    __psynch_mutexwait   <-- blocked here

nsLookAndFeel::EnsureInit() creates a hidden AppKit window to probe the system theme. AppKit immediately tries to resolve the system font to render the window’s title bar, which goes through CoreText into libFontRegistry.dylib, where it blocks trying to acquire the TLocalFontRegistry mutex.

I could not determine from the profile which thread was holding that mutex. A sampled stack records the thread that is waiting, not the thread that owns the lock, so the holder is never directly visible. What the profile does show is that the main thread is blocked in the TLocalFontRegistry constructor and that the only other threads touching that registry during this window are the two font threads below.

What the font threads are doing

On the RegisterFonts thread, gfxPlatformMac::FontRegistrationCallback is scanning font directories and registering them with the OS. It calls CoreTextFontList::ActivateFontsFromDir, which goes through CTFontManagerRegisterActionFontURLs and into libFontRegistry.dylib’s registration path. Adding fonts to the index almost certainly requires the TLocalFontRegistry mutex, which would make this thread a holder for part of the window, but that is a mechanism inference from the call stack rather than something the samples show.

What the samples do show is that RegisterFonts is itself contending: 20.9% of its samples are in __psynch_mutexwait via TLocalFontRegistry::TLocalFontRegistry(), the same frame the main thread blocks in. So it cannot be the holder throughout: for a fifth of its samples it is a waiter too. Most of the remaining time is real registration work, mostly filesystem calls (stat 9.2%, __getattrlist 6.1%) plus string and URL construction, with a further 3.7% waiting on synchronous XPC to the font daemon.

On InitFontList, gfxPlatformFontList::InitFontList() calls CTFontDescriptorCreateMatchingFontDescriptorsWithOptions to enumerate the system font families. This also needs the font registry, and about 16% of the thread’s samples (17 of 107) are spent in synchronous XPC replies waiting for the font daemon to respond. That figure has to come from a search across the whole thread: in the top-down tree the XPC wait frame is split across two branches, at 10.3% and 4.7%, so reading either branch alone understates it.

The picture

All three threads need the TLocalFontRegistry mutex at overlapping times. The font threads need it to register and index fonts. The main thread needs it because nsLookAndFeel::EnsureInit() creates an AppKit window that immediately asks CoreText for the system font.

The lock contention is concentrated in the first 400ms because that is when both the XPCOM initialization path and the font registration threads are active simultaneously. After the font threads finish, the contention disappears and the main thread continues normally.

The “low-CPU gaps” visible in the profiler timeline on the main thread during startup are these 112 samples blocked in the kernel waiting for the font registry mutex. The main thread is not genuinely idle: it is ready to work but stalled on a lock.

Worth separating what this profile establishes from what it does not, because the distinction matters when filing this in a bug. Established: all three threads block in TLocalFontRegistry::TLocalFontRegistry(), the main thread loses 112 of the window’s 394 samples to it, and the contention is confined to the first 400ms. Not established: which thread owns the mutex at any given moment. Pinning that down needs lock-contention instrumentation or a dtrace/lldb session, not a sampled profile.