Skip to content

Fix NSHostingView async rendering cycle on macOS#898

Merged
Kyle-Ye merged 1 commit into
mainfrom
fix/nshostingview-async-cycle
Jun 7, 2026
Merged

Fix NSHostingView async rendering cycle on macOS#898
Kyle-Ye merged 1 commit into
mainfrom
fix/nshostingview-async-cycle

Conversation

@Mx-Iris

@Mx-Iris Mx-Iris commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a cross-start/stop async rendering loop in NSHostingView triggered by TimelineView(.animation) on macOS.

struct BreathingColorView: View {
    var body: some View {
        TimelineView(.animation) { timeline in
            let time = timeline.date.timeIntervalSince1970
            let breathe = (sin(time) + 1) / 2 // Oscillates between 0 and 1
            
            VStack(spacing: 40) {
                Text(verbatim: "Breathing Colors")
                    .font(.title)
                    .fontWeight(.bold)

                Color.blue.opacity(0.3 + breathe * 0.7)
                    .frame(width: 200, height: 200)
                    .scaleEffect(0.8 + breathe * 0.4)
                
                Color.blue
                    .frame(width: 200, height: 20)
                    .opacity(0.3 + breathe * 0.7)
                    .cornerRadius(10)
            }
            .padding()
        }
    }
}

Root cause

When requestUpdate(after:) is called during render() (isUpdating == true) with a non-zero delay — e.g. ~16 ms from TimelineView's next-frame schedule — OpenSwiftUI directly set needsDeferredUpdate = true, which caused the layout() closure to call startAsyncRendering() and spin up a CoreDisplayLink. renderAsync typically returned nil (e.g. due to pending transactions or incomplete updateOutputsAsync), destroying the display link and queuing another requestUpdate(after: 0) — repeating every layout pass.

Changes

  • requestUpdate(after:): match SwiftUI's delay == 0 || !isUpdating branch.
  • setNeedsUpdate(): when isUpdating, set needsDeferredUpdate = true instead of calling setNeedsLayout reentrantly.
  • layout() inner closure: repeat-while (max 8 iterations) with !viewGraph.mayDeferUpdate guard before startAsyncRendering(). Drop the OSUI-only onNextMainRunLoop { setNeedsUpdate() } fallback (not present in SwiftUI).

iOS / visionOS hosting paths (UIHostingViewBase) are unaffected — this change is #if os(macOS) only.

Align requestUpdate(after:), setNeedsUpdate(), and layout() with
SwiftUI 6.5.x so TimelineView no longer triggers a repeated
CoreDisplayLink create/destroy cycle on macOS.

- requestUpdate(after:): match SwiftUI's "delay == 0 || !isUpdating"
  branch. When delay > 0 and isUpdating, schedule setNeedsUpdate()
  via onNextMainRunLoop instead of setting needsDeferredUpdate.
- setNeedsUpdate(): when isUpdating, set needsDeferredUpdate
  instead of triggering a reentrant setNeedsLayout.
- layout() inner closure: repeat-while (max 8 iterations) with a
  !viewGraph.mayDeferUpdate guard before startAsyncRendering(),
  matching SwiftUI's structure. Drop the OSUI-only fallback.
@github-actions github-actions Bot added area: hosting-bridge SwiftUI bridge, UIHosting/NSHosting, representables, and platform host views. area: rendering DisplayList, render backends, renderer hosts, drawing, and effects. platform: macOS macOS-specific behavior or support. type: bug Something is not working correctly. labels Jun 7, 2026
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.16667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.70%. Comparing base (fb8f421) to head (98e1059).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ntegration/Hosting/AppKit/View/NSHostingView.swift 79.16% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #898      +/-   ##
==========================================
- Coverage   26.71%   26.70%   -0.02%     
==========================================
  Files         697      697              
  Lines       48907    48910       +3     
==========================================
- Hits        13066    13061       -5     
- Misses      35841    35849       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Mx-Iris Mx-Iris marked this pull request as ready for review June 7, 2026 15:23
@Mx-Iris Mx-Iris requested a review from Kyle-Ye as a code owner June 7, 2026 15:23
Copilot AI review requested due to automatic review settings June 7, 2026 15:23
@augmentcode

augmentcode Bot commented Jun 7, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Prevents a macOS NSHostingView async rendering start/stop loop triggered by TimelineView(.animation).

Changes:

  • Aligns requestUpdate(after:) with SwiftUI: call setNeedsUpdate() only for delay == 0 or when not updating; otherwise defer to the next main run loop.
  • Updates setNeedsUpdate() to avoid re-entrant layout during render() by setting needsDeferredUpdate instead.
  • Reworks layout() to drain deferred updates via a bounded repeat loop and guard startAsyncRendering() with !viewGraph.mayDeferUpdate.

Technical Notes: macOS-only behavior change intended to match SwiftUI 26.5 NSHostingView scheduling and avoid unnecessary CoreDisplayLink churn.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

needsDeferredUpdate = false
}
iteration += 1
} while needsDeferredUpdate && iteration < 8

@augmentcode augmentcode Bot Jun 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If needsDeferredUpdate remains true after hitting the iteration < 8 cap, there’s no follow-up scheduling (e.g. another setNeedsUpdate()), so the view could drop an update and get stuck. Is it guaranteed that this condition can’t persist beyond the loop?

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the macOS NSHostingView update scheduling and layout/render loop behavior to avoid a cross start/stop async rendering cycle triggered by TimelineView(.animation), aligning behavior with the macOS 26.5 SwiftUI NSHostingView binary.

Changes:

  • Updates requestUpdate(after:) so that updates requested during render() with a non-zero delay are scheduled via onNextMainRunLoop { setNeedsUpdate() } (instead of toggling needsDeferredUpdate and entering the async rendering path).
  • Changes setNeedsUpdate() to avoid re-entrant layout scheduling while isUpdating by setting needsDeferredUpdate instead.
  • Reworks layout()’s internal render loop to repeat while deferred updates are requested (up to 8 iterations) and only start async rendering when !viewGraph.mayDeferUpdate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

needsDeferredUpdate = false
}
iteration += 1
} while needsDeferredUpdate && iteration < 8
@Kyle-Ye

Kyle-Ye commented Jun 7, 2026

Copy link
Copy Markdown
Member

/uitest macos

@Kyle-Ye Kyle-Ye merged commit f46200a into main Jun 7, 2026
9 of 11 checks passed
@Kyle-Ye Kyle-Ye deleted the fix/nshostingview-async-cycle branch June 7, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: hosting-bridge SwiftUI bridge, UIHosting/NSHosting, representables, and platform host views. area: rendering DisplayList, render backends, renderer hosts, drawing, and effects. platform: macOS macOS-specific behavior or support. type: bug Something is not working correctly.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants