Revision control

Copy as Markdown

Other Tools

# Firefox macOS Startup: Font Initialization Lock Contention
## Load the profile
```
Session started: default
```
## Get an overview
```
$ profiler-cli profile info
[Thread: ... | View: Full profile | Full: 1.59s]
Name: Firefox 142 – macOS 15.5.0
Platform: macOS 15.5.0
This profile contains 19 threads across 11 processes.
Top processes and threads by CPU usage:
p-0: Parent Process [pid 10860] [ts<0z → end] - 1236.908ms
t-0: GeckoMain [tid 3590310] - 814.960ms
t-2: Renderer [tid 3590371] - 190.684ms
t-1: RegisterFonts [tid 3590316] - 120.713ms
t-5: InitFontList [tid 3590386] - 87.176ms
...
```
Three threads in the parent process are interesting: the main thread, `RegisterFonts`, and `InitFontList`. The font threads are running during startup at the same time as the main thread.
## Confirm lock contention on the main thread
Select the main thread and search for `psynch`, the macOS kernel call that a thread lands in when blocked on a mutex:
```
$ profiler-cli thread select t-0
Selected thread: t-0 (GeckoMain)
$ profiler-cli zoom push 0,0.4
Pushed view range: ts-0 (0s) to ts-d (400ms) (duration: 400.00ms)
$ profiler-cli thread samples --search "psynch" --include-idle
[Thread: t-0 (GeckoMain) | View: ts-0→ts-d (400.0ms) | Full: 1.59s]
Top Functions (by total time):
f-0. (root) - total: 112 (100.0%)
...
f-691. libsystem_pthread.dylib!_pthread_mutex_firstfit_lock_slow - total: 97 (86.6%)
f-692. libsystem_kernel.dylib!__psynch_mutexwait - total: 97 (86.6%)
f-753. libFontRegistry.dylib!TLocalFontRegistry::TLocalFontRegistry() - total: 93 (83.0%)
...
```
The first 400ms has 394 total samples. 112 of them contain `psynch` in the stack, all landing in `__psynch_mutexwait` via `TLocalFontRegistry::TLocalFontRegistry()`. That is 28% of the window blocked on the font registry mutex.
All 112 of these samples are in the first 400ms (the count does not change when the zoom is removed), confirming the contention is early and concentrated.
## Trace the call chain
```
$ profiler-cli zoom clear
$ profiler-cli thread samples-top-down --search "mutex"
```
The heaviest path through the mutex wait traces back to LookAndFeel initialization:
```
NS_InitXPCOM
nsComponentManagerImpl::Init
nsLayoutModuleInitialize
nsLayoutStatics::Initialize
nsContentUtils::Init
nsXPLookAndFeel::GetInstance
nsLookAndFeel::EnsureInit
[NSWindow initWithContentRect:...]
[NSThemeFrame _updateTitleProperties:...]
[NSTextFieldCell initTextCell:]
UIFoundation!+[NSFont systemFontOfSize:width:]
CoreText!TDescriptor::CreateMatchingDescriptorInternal
CoreText!MakeSpliceDescriptor
libFontRegistry.dylib!XTCopyFontWithName
libFontRegistry.dylib!TLocalFontRegistry::TLocalFontRegistry()
_pthread_mutex_firstfit_lock_slow
__psynch_mutexwait
```
`nsLookAndFeel::EnsureInit()` creates a hidden AppKit window to probe the system theme. AppKit immediately resolves the system font for the title bar, which walks into `libFontRegistry.dylib` and tries to acquire the `TLocalFontRegistry` mutex. That mutex is held by one of the font threads.
## What the font threads are doing
```
$ profiler-cli thread select t-1
Selected thread: t-1 (RegisterFonts)
$ profiler-cli thread samples-top-down
```
The `RegisterFonts` thread is scanning font directories and calling `CoreTextFontList::ActivateFontsFromDir`, which holds the `TLocalFontRegistry` mutex while adding fonts to the index. It also competes for the same lock:
```
Top-Down Call Tree:
f-0. (root) [total: 100.0%, self: 0.0%]
└─ ...
f-10357. XUL!gfxPlatformMac::FontRegistrationCallback(void*) [total: 100.0%, self: 0.0%]
├─ f-10358. XUL!CoreTextFontList::ActivateFontsFromDir(...) [total: 99.4%, self: 0.0%]
│ ├─ f-10364. CoreText!_CTFontManagerRegisterActionFontURLs() [total: 98.8%, self: 0.0%]
│ │ ├─ f-10412. libFontRegistry.dylib!CopyFaceURLsForFonts(...) [total: 47.9%, self: 0.0%]
│ │ │ f-887. libFontRegistry.dylib!XTCopyFontsWithProperties [total: 47.9%, self: 0.0%]
│ │ │ ├─ f-890. ...CopyPropertiesForFontsMatchingRequest()... [total: 27.0%, self: 0.0%]
│ │ │ │ ...
│ │ │ └─ f-753. libFontRegistry.dylib!TLocalFontRegistry::TLocalFontRegistry() [total: 20.9%, self: 0.0%]
│ │ │ f-691. libsystem_pthread.dylib!_pthread_mutex_firstfit_lock_slow [total: 20.9%, self: 0.0%]
│ │ │ f-692. libsystem_kernel.dylib!__psynch_mutexwait [total: 20.9%, self: 20.9%]
```
The `RegisterFonts` thread itself spends 20.9% of its samples blocked on `__psynch_mutexwait`. It is both the cause of the main thread's blocking and a contention victim itself.
```
$ profiler-cli thread select t-5
Selected thread: t-5 (InitFontList)
$ profiler-cli thread samples-top-down
```
The `InitFontList` thread calls `CTFontDescriptorCreateMatchingFontDescriptorsWithOptions` to enumerate font families, which also touches the font registry and makes synchronous XPC calls to the font daemon (10.3% of this thread's time in `__NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__`).
## Summary
All three threads need the `TLocalFontRegistry` mutex at overlapping times:
- `RegisterFonts` holds it while registering fonts from disk
- `InitFontList` needs it to enumerate font families (and also waits on XPC)
- `GeckoMain` needs it because `nsLookAndFeel::EnsureInit()` creates an AppKit window that immediately asks CoreText for the system font
The result is that the main thread spends about 28% of the first 400ms blocked in the kernel waiting for this single mutex. The low-CPU appearance in the profiler timeline during startup reflects this: the thread is not idle, it is ready to work but stalled on a lock held by the font threads.