Skip to content

Fix 100% CPU spin on macOS 26 caused by .truncationMode(.middle)#1382

Open
zhouyeyu wants to merge 1 commit intop0deje:masterfrom
zhouyeyu:fix/macos26-cpu-spin-truncation-middle
Open

Fix 100% CPU spin on macOS 26 caused by .truncationMode(.middle)#1382
zhouyeyu wants to merge 1 commit intop0deje:masterfrom
zhouyeyu:fix/macos26-cpu-spin-truncation-middle

Conversation

@zhouyeyu
Copy link
Copy Markdown

Problem

On macOS 26, Maccy's CPU usage climbs to 100% immediately after launch,
causing the app to become unresponsive. Reproduces on every open of the popup.

Root Cause

NSCoreTypesetter.__NSCoreTypesetterTruncateLine (in UIFoundation) enters
an infinite loop when measuring Text views with .truncationMode(.middle)
under an unconstrained height size proposal.

The trigger path:

  1. SlideoutView preview panel starts opening (isAnimating = true)
  2. Content VStack gets .fixedSize(horizontal: true, vertical: false)
  3. SwiftUI measures the content at ideal (unconstrained) height
  4. Traverses down: ZStack → VStack → SlideoutView → ScrollView → LazyVStack
  5. Each HistoryItemView title is measured via NSAttributedString.boundingRect
  6. CoreText's truncation loop hangs indefinitely for certain text content

Confirmed via sample profiling: 100% of CPU time in
__NSCoreTypesetterTruncateLine_block_invoke.

Fix

  • Use .truncationMode(.tail) on macOS 26 in ListItemTitleView.
    The existing .drawingGroup() workaround for flipped text (Flipped labels in macOS Tahoe 26.0 #1113)
    is preserved and works correctly with tail truncation.
  • (Defensive) HeightReaderModifier: write to @Observable keypaths
    directly instead of via a custom Binding to avoid unintended render
    dependency registration.
  • (Defensive) AppDelegate / HistoryItemDecorator: guard
    withObservationTracking re-registration callbacks with a pending flag
    to prevent observation count from growing when multiple mutations fire
    in the same turn.

On macOS 26, NSCoreTypesetter's __NSCoreTypesetterTruncateLine enters
an infinite loop when measuring text with .truncationMode(.middle) under
an unconstrained height proposal. This occurs during the SlideoutView
preview-panel animation, where .fixedSize(horizontal: true) causes
SwiftUI to measure the LazyVStack content at ideal (unconstrained) height,
triggering text measurement for every history item via that code path.

Fix: use .truncationMode(.tail) on macOS 26 to avoid the CoreText bug.
The .drawingGroup() workaround for flipped text (p0deje#1113) is preserved and
remains safe with .tail truncation.

Additional defensive improvements:
- HeightReaderModifier: write to @observable keypaths directly instead
  of via @binding to prevent unintended render dependency registration.
- AppDelegate / HistoryItemDecorator: add pending-flag guards to the
  withObservationTracking re-registration loops to prevent unbounded
  observation accumulation when multiple mutations fire in one turn.
@jaspermayone
Copy link
Copy Markdown

I wasn't able to produce this on macos26....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants