Skip to content

Fix crash: infinite layout loop in timeline with media/reactions#84

Merged
viktorstrate merged 4 commits intoviktorstrate:mainfrom
beezly:fix/timeline-layout-crash
Mar 17, 2026
Merged

Fix crash: infinite layout loop in timeline with media/reactions#84
viktorstrate merged 4 commits intoviktorstrate:mainfrom
beezly:fix/timeline-layout-crash

Conversation

@beezly
Copy link
Contributor

@beezly beezly commented Mar 16, 2026

Problem

Opening a room containing video/image messages with reactions or read receipts causes an infinite layout loop that crashes the app with NSGenericException:

The window has been marked as needing another Update Constraints in Window pass, but it has already had more Update Constraints in Window passes than there are views in the window.

The crash is timing-dependent — it reproduces when launched normally but not always under a debugger.

Root Cause

NSHostingView inside NSTableView with usesAutomaticRowHeights enters an infinite constraint update cycle. During layout, NSHostingView.layout() flushes SwiftUI transactions, which can invalidate constraints (especially when the SwiftUI view tree structure changes), which re-enters layout — exceeding AppKit's re-entrancy limit.

The crash was latent since the NSTableView rewrite (#47). Adding view complexity (reactions, read receipts, video playback) increased the likelihood of triggering the loop.

Fix

Four commits, each addressing a contributing factor:

1. Guard against zero width/height in image aspect ratio

Prevent division by zero when image metadata reports zero or missing dimensions, which produces infinity/NaN aspect ratios.

2. Video sizing fixes

  • Same zero-division guards as images
  • Correct modifier order: .aspectRatio() before .frame(maxHeight:)
  • Add minHeight for videos without thumbnails (otherwise MatrixImageView renders a zero-intrinsic-size rectangle that collapses under .aspectRatio(.fit))

3. Stabilise view tree in MessageEventBodyView

Replace conditional if/else if blocks for reactions and read receipts with an always-present HStack that uses frame(height: 0) + .clipped() when empty. Conditional view branches change the SwiftUI view tree structure when data loads asynchronously, triggering invalidateIntrinsicContentSize during layout.

4. Replace usesAutomaticRowHeights with deferred self-sizing

SelfSizingHostingView subclass that:

  • Sets sizingOptions = [] to opt out of Auto Layout constraint solving
  • Overrides invalidateIntrinsicContentSize() to schedule a coalesced async height update instead of calling super (which would re-enter the constraint system)
  • measureHeight() temporarily re-enables sizing, pins a width constraint, reads fittingSize, then resets — all outside the layout pass
  • Row heights are cached and updated via noteHeightOfRows with animation disabled

Also removes the old measurementHostingView (separate NSHostingController per height query) and handleTableResize observer.

@beezly beezly marked this pull request as draft March 16, 2026 21:30
@beezly beezly force-pushed the fix/timeline-layout-crash branch 2 times, most recently from dba5ab2 to 9fc07da Compare March 16, 2026 22:45
@beezly
Copy link
Contributor Author

beezly commented Mar 16, 2026

I've based this commit off #80 as it includes changes to similar code. It should show the same two commits as 80 until that branch is merged, and then should just have the last four.

@beezly beezly marked this pull request as ready for review March 16, 2026 22:46
@viktorstrate
Copy link
Owner

Bugs found when testing it

  • When I clicked play on a video, the video player became very small.

    Screenshot 2026-03-17 at 09 00 07
  • It doesn't seem to handle wrapping properly. Even after loading a room from scratch (no resizing after timeline load) can this happen.

Screenshot 2026-03-17 at 09 05 52
  • By resizing the window to a small width, I managed to hit an infinite loop, when pausing the debugger, I placed in the sizeThatFits function. I'm not sure this is related to this PR and it might as well be an unrelated old bug.

    Screenshot 2026-03-17 at 09 05 05

Comments

I think the layout of the timeline is very complicated, so I think we need to approach this problem very carefully. I am suspecting that commit 4 is the one causing the rows to have the wrong height, since it's the only one that touches the AppKit level layouting. And that commit 1-3 could be standalone improvements independent of 4.

If it's easier we can split this PR up into 1-3, make sure they fully work and merge them, and then we can focus on improving the AppKit layout in a separate PR. What do you think?

@beezly
Copy link
Contributor Author

beezly commented Mar 17, 2026 via email

@beezly beezly force-pushed the fix/timeline-layout-crash branch from 9fc07da to 5c18560 Compare March 17, 2026 09:11
@beezly
Copy link
Contributor Author

beezly commented Mar 17, 2026

I ran a test build of this and wrapping is correct.

beezly added 3 commits March 17, 2026 11:05
Prevent division by zero when image metadata reports zero or missing
dimensions. Without this guard, aspectRatio computes infinity or NaN,
which causes SwiftUI layout issues inside NSTableView cells.
…llback

- Guard against zero width/height in aspect ratio calculation (same as
  image fix).
- Reorder modifiers: .aspectRatio() before .frame(maxHeight:) so the
  aspect ratio is applied before the height constraint, matching the
  intended sizing behaviour.
- Add minHeight when thumbnailSource is nil. Without a thumbnail,
  MatrixImageView renders a Rectangle with no intrinsic size, causing
  .aspectRatio(.fit) to collapse the video to zero height.
Replace the conditional if/else if blocks for reactions and read
receipts with an always-present HStack that uses frame(height: 0)
and .clipped() when empty.

Conditional view branches (if !reactions.isEmpty / else if) change
the SwiftUI view tree structure when data loads asynchronously. Inside
an NSHostingView table cell, this triggers invalidateIntrinsicContentSize
during layout, contributing to an infinite constraint update loop that
crashes the app. Keeping the view tree stable avoids this trigger.
beezly added a commit to beezly/mactrix that referenced this pull request Mar 17, 2026
@viktorstrate viktorstrate merged commit 063e3a2 into viktorstrate:main Mar 17, 2026
2 checks passed
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