Repository navigation
Description
Summary
On the macOS desktop app, the UI froze for seconds at a time while the window was in use. One interaction took 26.5 s to paint, and replies that the server answered in under 0.1 ms took up to 10.4 s to reach the screen. The profiler raised rpc.slow for server.getProcessDiagnostics (now error), previewAutomation.focusHost and server.getProviderUsage, together with client.stall (#99) and client.interaction (#103). Outside the profiler, the WebKit WebContent process that runs the BiBCode UI was at 55 to 102 % CPU and 2.75 GB RSS (1015 MB physical footprint). A 5 s sample of that process puts about 73 % of the main thread in WebKit's LabelsNodeList traversal, which is what element.labels[i] does. In the web dependencies, only Base UI's useAriaLabelledBy reads .labels, and it reads it after every commit of every Switch, Checkbox and Radio.
Environment
| Field | Value |
|---|---|
| BiBCode | 0.9.2, desktop (DMG) |
| OS | macOS 27.0.1, arm64 |
| Mode | local |
| Finding | rpc.slow-ee0e8d26aa17: error, seen 19 times from 2026-10-12T13:26:54Z to 2026-10-14T19:13:12Z |
| Fingerprint | ee0e8d26aa17 |
| Capture | native dumps available, OS crash records available |
Same incident, same client, also covered here:
rpc.slow-37b7a238df64,server.getProcessDiagnostics: warn, 13 times, last 2026-10-14T19:13:12Zrpc.slow-d5be99ded9ca,previewAutomation.focusHost: warn, 2 times, last 2026-10-14T19:12:05Zrpc.slow-006a9e582a40andrpc.slow-e12cdb23a0d5,server.getProviderUsage: warn, 2 times each, last 2026-10-14T19:13:12Z
Evidence
97f7ca61-5143client.requestserver.getProcessDiagnostics: total 10,422 ms, server 0.07 ms, network 7,047 ms, render 3,375 ms. With the server in-process, most of that "network" time is likely the reply waiting for the busy main thread.97f7ca61-5145client.requestserver.getProcessDiagnostics: total 3,309 ms, server 0.04 ms, render 2,667 ms.97f7ca61-5122client.requestpreviewAutomation.focusHost: total 9,610 ms, server 0.01 ms, render 9,086 ms.97f7ca61-5114,97f7ca61-5148client.requestserver.getProviderUsage: totals 2,290 and 1,091 ms, server 0.02 ms each, render 1,638 and 530 ms.97f7ca61-5150client.batch(theclient.stallfinding): 6 stalls, 13,600 ms total, the longest 6,189 ms, in a 10,001 ms window, so not timer throttling (compare client.stall: heartbeat counts WebKit timer throttling as stalls on macOS #99).97f7ca61-5132client.batch(theclient.interactionfinding): 4 slow interactions, the slowest 26,464 ms.97f7ca61-5156process.sample: the desktop host (with the server in-process) at 0.5 % CPU. Its private memory went from 74 MB to 169 MB at about 19:11Z (97f7ca61-5108,-5135) and then stayed flat. The WebContent process does not appear inprocess.sample(see profiler: macOS desktop never samples its WebKit WebContent process #104).- RPC traffic in the 5 minutes before: 19
server.refreshProviderUsage, 15server.getProcessDiagnostics, 4server.getProviderUsage, 2previewAutomation.focusHost. sampleof the WebContent process, 5 s at 1 ms: 1,980 samples on the main thread. 1,438 run from JIT JavaScript throughoperationGetByValGeneric→WebCore::JSNodeList::getOwnPropertySlotByIndex→WebCore::CachedLiveNodeList<WebCore::LabelsNodeList>::item→WebCore::CollectionTraversal<…>::begin<WebCore::LabelsNodeList>. That is an indexed read,labels[0], on a livelabelslist whose cache was invalidated, so WebKit walks the document to find the first label.footprintof the WebContent process: 1015 MB, of which 647 MB is dirty WebKit Malloc.
Stack
WebContent main thread, top first (system frames, from sample):
#0 WebCore::CollectionTraversal<1>::begin<WebCore::LabelsNodeList>
#1 WebCore::CachedLiveNodeList<WebCore::LabelsNodeList, 1>::item(unsigned int)
#2 WebCore::JSNodeList::getOwnPropertySlotByIndex
#3 operationGetByValGeneric (JavaScriptCore)
#4 … JIT JavaScript (unsymbolicated)
Suspected source
@base-ui/react1.7.0,internals/labelable-provider/useAriaLabelledBy.js:useIsoLayoutEffectwithout dependencies, so it runs after every commit. Unless the control has an explicitaria-labelledbyor label id, it callsfindAssociatedLabel, which ends inlabelSource.labelsandlabels[0]. Used bySwitchRoot,CheckboxRoot,RadioRootandOTPFieldRoot. A search of the web dependencies for.labelsreads finds no other browser-side caller.apps/web/src/components/ui/switch.tsx:7-28: the sharedSwitchpasses noaria-labelledby, so every switch takes the fallback. The same goes for theui/checkbox.tsxandui/radio-group.tsxwrappers.<Switch,<Checkboxand<Radioappear 77 times across 33 files, with 11 incomponents/settings/SettingsPanels.tsx.apps/web/src/components/status-bar/AppStatusBar.tsx:134: the 2 sserver.getProcessDiagnosticspoll, plus the provider-usage refresh. Each reply that commits under a mounted control repeats the label scan, so the cost is roughly polls × controls × document size.
Reproduction
- In the macOS desktop app with the Profiler on, open screens that mount many Base UI switches or checkboxes (Settings, especially with About → View diagnostics open), and keep a large conversation loaded so the document is big.
- Leave the window focused for a few minutes while the status-bar polls run.
- Watch
ps -o %cpu,rss -p <WebContent pid>(the WebContent process started together withbibcode-desktop) and runsample <pid> 5. ExpectLabelsNodeListtraversal to dominate. - Run
profiler findings --since 10m --json. Expectrpc.slowfindings with server time near 0 and large render or network times, plusclient.stall.
Root-cause hypothesis
Base UI's label fallback reads the live labels list of every labelable control after every React commit. In WebKit, that list is invalidated by any DOM change, so each read walks the document. With dozens of controls mounted, a large DOM and commits driven by polling every 2 s, the main thread is busy all the time, and RPC replies queue behind it. The 2.75 GB RSS and 647 MB of dirty WebKit Malloc are not explained by this scan. They may be the macOS counterpart of #96's webview growth and should be tracked there. Confidence: high that labels[0] traversal dominates the main thread (direct sample); medium that Base UI's fallback is the caller (the only browser-side .labels reader among the dependencies, but the JavaScript frames are unsymbolicated); low on which mounted screen contributes most.
Suggested fix direction
- Give every Base UI Switch, Checkbox, Radio and OTP field an explicit
aria-labelledby, or wrap it in Base UIFieldwith aField.Label, solabelIdis set and the fallback never runs. Enforce this in theui/wrappers, for example by requiring anaria-labeloraria-labelledbyprop. - Report the per-commit
.labelsread to Base UI, which could run the fallback only when the DOM around the control changes. - Stop status-bar polls from committing when the data has not changed, and pause them while the window is hidden or occluded.
- Confirm with a Web Inspector timeline on the desktop build after the fix:
LabelsNodeListshould be gone fromsample, and WebContent CPU should be near idle.
Activity
mubeda commentedon Oct 14, 2026
New occurrence of
rpc.slow-2cb2caf8ee75(profiler fingerprint2cb2caf8ee75), the same client and cause as this issue:What is new: a sixth fingerprint from the same incident.
previewAutomation.focusHosttook 2,012 ms (server 0 ms, network 1,676 ms, render 336 ms).Still active at 2026-10-14T21:25Z: the WebContent process is at 8 to 100 % CPU and 2.90 GB RSS (it was 2.75 GB at 19:14Z). A new 5 s
sampleagain showsWebCore::LabelsNodeListtraversal as the top non-idle frame (895 samples).client.stall-93202704f780reached 589 and the errorrpc.slow-ee0e8d26aa17reached 34.fix(web): stop Base UI scanning the page for labels after every render (
merge: release v0.9.3 (PR #108)
mubeda commentedon Oct 15, 2026
Shipped in v0.9.3 (678fc29).
Base UI 1.9 skips the page-wide label lookup for controls that already have a label, and the file checkboxes in the commit dialog now have one. A regression test counts
labelsreads across 20 commits of every switch, checkbox and radio (80 reads before, 0 after).mubeda commentedon Oct 27, 2026
New occurrence of
rpc.slow-ee0e8d26aa17(profiler fingerprintee0e8d26aa17):What is new: the finding came back on 0.12.0 with a different slow part. Render took only 24 ms, so the label-lookup render saturation fixed above is not the cause. Almost all of the wait was network time.
server.getProcessDiagnostics, total 1025 ms = server 0.17 ms + network 1001 ms + render 24 ms, slow partnetwork, joined by correlation id (event19a12444-688).server.refreshProviderUsagereply. That call had just taken 15,148 ms on the server (rpc.timing, tiersevere, event19a12444-687, finished 20:50:58.154Z). The diagnostics reply may have waited behind the provider-usage completion on the shared connection or in the client's receive path.server.refreshProviderUsageis listed inis_long_running(apps/server/src/diagnostics/profiler/latency.rs:146), so its own duration raises no finding.