Repository navigation
Fix idle CPU usage from animation timeline and git polling #2561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
358bd7c
2c5d8f5
0a876f9
7cc27e6
f91d201
792e1c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -102,14 +102,25 @@ struct TmuxWorkspacePaneOverlayRenderState: Equatable { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @MainActor | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| final class TmuxWorkspacePaneOverlayModel: ObservableObject { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Published private(set) var unreadRects: [CGRect] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Published private(set) var flashRect: CGRect? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Published private(set) var flashStartedAt: Date? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Published private(set) var flashReason: WorkspaceAttentionFlashReason? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| final class TmuxWorkspacePaneOverlayModel { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private(set) var unreadRects: [CGRect] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private(set) var flashRect: CGRect? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private(set) var flashStartedAt: Date? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private(set) var flashReason: WorkspaceAttentionFlashReason? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var lastWorkspaceId: UUID? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var lastFlashToken: UInt64? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var lastObservedFlashToken: UInt64? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var runningFlashToken: UInt64? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var flashResetWorkItem: DispatchWorkItem? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var onStateChange: (() -> Void)? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| var showsAnimatedFlash: Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashRect != nil && flashStartedAt != nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| var hasVisibleContent: Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| !unreadRects.isEmpty || showsAnimatedFlash | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| func apply( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ state: TmuxWorkspacePaneOverlayRenderState, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -122,26 +133,88 @@ final class TmuxWorkspacePaneOverlayModel: ObservableObject { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| let didChangeWorkspace = lastWorkspaceId != state.workspaceId | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if didChangeWorkspace { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastWorkspaceId = state.workspaceId | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastFlashToken = state.flashToken | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastObservedFlashToken = state.flashToken | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningFlashToken = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cancelFlashReset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashStartedAt = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let lastFlashToken, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.flashToken != lastFlashToken, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let didChangeFlashToken = lastObservedFlashToken != state.flashToken | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if didChangeFlashToken { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastObservedFlashToken = state.flashToken | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if state.flashRect != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| startFlash( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspaceId: state.workspaceId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashToken: state.flashToken, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| startedAt: now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A new token supersedes any in-flight flash even when layout churn | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // temporarily leaves the pane without a window-space rect. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cancelFlashReset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashStartedAt = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningFlashToken = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Preserve an in-flight flash if the pane rect is temporarily missing. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Once the same token has a rect again, resume from the original start time. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if runningFlashToken == nil, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.flashRect != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashStartedAt = now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| startFlash( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspaceId: state.workspaceId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashToken: state.flashToken, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| startedAt: now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.lastFlashToken = state.flashToken | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| func clear() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cancelFlashReset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| unreadRects = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashRect = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashStartedAt = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashReason = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastWorkspaceId = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastFlashToken = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastObservedFlashToken = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningFlashToken = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func startFlash(workspaceId: UUID, flashToken: UInt64, startedAt: Date) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashStartedAt = startedAt | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| runningFlashToken = flashToken | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| scheduleFlashReset(workspaceId: workspaceId, flashToken: flashToken) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func scheduleFlashReset(workspaceId: UUID, flashToken: UInt64) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cancelFlashReset() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| let workItem = DispatchWorkItem { [weak self] in | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let self else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard self.lastWorkspaceId == workspaceId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.runningFlashToken == flashToken else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.flashStartedAt = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Keep the handled token latched so later layout churn with the same | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // token cannot replay a completed flash. A new token or workspace | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // change will reset the latch. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.onStateChange?() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashResetWorkItem = workItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| DispatchQueue.main.asyncAfter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| deadline: .now() + FocusFlashPattern.duration, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| execute: workItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+195
to
+212
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Wrapping the body with
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func cancelFlashReset() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashResetWorkItem?.cancel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flashResetWorkItem = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -152,29 +225,50 @@ struct TmuxWorkspacePaneOverlayView: View { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| let flashReason: WorkspaceAttentionFlashReason? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| var body: some View { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TimelineView(.animation) { timeline in | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Canvas { context, _ in | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for rect in unreadRects { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| drawUnreadRing(in: &context, rect: rect) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Group { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if flashRect != nil && flashStartedAt != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TimelineView(.animation) { timeline in | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Canvas { context, _ in | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| renderOverlay( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| in: &context, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentDate: timeline.date | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let flashRect, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let flashStartedAt else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let elapsed = timeline.date.timeIntervalSince(flashStartedAt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let opacity = FocusFlashPattern.opacity(at: elapsed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard opacity > 0.001 else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| drawFlashRing( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| in: &context, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rect: flashRect, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: opacity, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| reason: flashReason ?? .notificationArrival | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if !unreadRects.isEmpty { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Canvas { context, _ in | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| renderOverlay(in: &context, currentDate: nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Color.clear | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .allowsHitTesting(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .frame(maxWidth: .infinity, maxHeight: .infinity) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func renderOverlay( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| in context: inout GraphicsContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentDate: Date? | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for rect in unreadRects { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| drawUnreadRing(in: &context, rect: rect) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let flashRect, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let flashStartedAt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let currentDate else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let elapsed = currentDate.timeIntervalSince(flashStartedAt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let opacity = FocusFlashPattern.opacity(at: elapsed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard opacity > 0.001 else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| drawFlashRing( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| in: &context, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rect: flashRect, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| opacity: opacity, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| reason: flashReason ?? .notificationArrival | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func drawUnreadRing(in context: inout GraphicsContext, rect: CGRect) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let path = ringPath(for: rect) else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| var glowContext = context | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latch the token after auto-clear.
Because Lines 202-203 clear
runningFlashToken, the sameflashTokenfalls back into the Lines 165-171 start path on the nextapply(...)with a non-nilrect. Any later layout/unread update can replay the flash without a new attention event, which risks repeated animation work after the flash was supposed to be finished.🛠️ Minimal fix
let workItem = DispatchWorkItem { [weak self] in guard let self else { return } guard self.lastWorkspaceId == workspaceId, self.runningFlashToken == flashToken else { return } self.flashStartedAt = nil - self.runningFlashToken = nil self.onStateChange?() }Also applies to: 202-203
🤖 Prompt for AI Agents