Skip to content

fix: resolve UI lag and high CPU usage during drag/resize#109

Merged
pablopunk merged 3 commits intopablopunk:mainfrom
gaorfg-bit:fix/performance-lag-coalescing
Mar 29, 2026
Merged

fix: resolve UI lag and high CPU usage during drag/resize#109
pablopunk merged 3 commits intopablopunk:mainfrom
gaorfg-bit:fix/performance-lag-coalescing

Conversation

@gaorfg-bit
Copy link
Copy Markdown
Contributor

@gaorfg-bit gaorfg-bit commented Mar 25, 2026

This PR implements 120Hz event coalescing and AX call deduplication to fix performance issues on high-refresh rate displays and gaming mice.

Summary by CodeRabbit

  • Bug Fixes

    • More reliable tracking across space changes and when stopping; avoids stale state and unnecessary toggles.
    • Suppresses redundant move/resize operations when position/size changes are negligible.
  • Performance

    • Smoother, more responsive mouse-driven move/resize via throttling and batched updates.
    • Faster and more accurate window detection/fallbacks for improved target selection.

@codesandbox
Copy link
Copy Markdown

codesandbox bot commented Mar 25, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26839f58-4907-4c07-ad79-5530d8cc422e

📥 Commits

Reviewing files that changed from the base of the PR and between 3ce7ba2 and 3da5b98.

📒 Files selected for processing (1)
  • Swift Shift/src/Manager/MouseTracker.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Swift Shift/src/Manager/MouseTracker.swift

📝 Walkthrough

Walkthrough

Introduces throttled/batched mouse updates and pending-state in MouseTracker with final-flush behavior; reduces redundant move/resize calls via last-applied caches; simplifies input/event handling and enums. WindowManager is condensed, adds shouldMoveOrigin to resize, and changes window lookup to prefer AX hit-testing then per-app kAXWindowsAttribute scanning.

Changes

Cohort / File(s) Summary
Mouse tracking core & input
Swift Shift/src/Manager/MouseTracker.swift
Adds throttling/batching state (minimumUpdateInterval, pendingMouseLocation, lastUpdateTime), last-applied caches (lastAppliedOrigin, lastAppliedSize), flushPendingMouseUpdate(...) with final flush on stop, avoids redundant move/resize calls, simplifies enums and key/event checks, and tightens space-change handling to active tracking only.
Window management & lookup
Swift Shift/src/Manager/WindowManager.swift
Compresses/inline refactors across WindowManager, reforms WindowBounds formatting, adds shouldMoveOrigin param to resize(window:to:from:shouldMoveOrigin:), changes getCurrentWindow() to prefer AX hit-testing and fallback to per-app kAXWindowsAttribute scanning, and simplifies PID→NSApplication lookup and role traversal.

Sequence Diagram(s)

sequenceDiagram
    participant Mouse as Mouse Events
    participant Tracker as MouseTracker
    participant WindowMgr as WindowManager
    participant System as Accessibility/OS

    Mouse->>Tracker: mouseMoved(location)
    Tracker->>Tracker: set pendingMouseLocation, update lastUpdateTime
    alt throttle interval elapsed
        Tracker->>WindowMgr: compute origin/size from pending location
        WindowMgr->>System: request AX/CG move or resize
        System-->>WindowMgr: ack
        WindowMgr-->>Tracker: report applied origin/size
    else within interval
        Tracker-->>Tracker: defer until flush
    end
    Mouse->>Tracker: stopTracking
    Tracker->>Tracker: flushPendingMouseUpdate (final)
    Tracker->>WindowMgr: final move/resize (if not approx-equal)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • refactor mouse events #83: Overlaps MouseTracker lifecycle and reset/start/stop handling changes; strong code-level similarity.

Poem

🐰 I hop with buffered, gentle taps,

I hold the moves in tiny maps,
I skip the nudges barely seen,
then flush them swift and quiet, clean,
a rabbit’s nudge keeps windows zapped ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: resolve UI lag and high CPU usage during drag/resize' accurately describes the main objective of the PR: implementing event coalescing and AX call deduplication to resolve performance issues. It is concise, specific, and clearly summarizes the primary change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
Swift Shift/src/Manager/WindowManager.swift (1)

3-3: Single-line struct reduces readability.

While functionally correct, condensing the struct to one line makes it harder to scan property names. Consider expanding for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Swift` Shift/src/Manager/WindowManager.swift at line 3, The one-line
declaration of the WindowBounds struct (symbol: WindowBounds with properties
topLeft, topRight, bottomLeft, bottomRight) reduces readability; expand it into
a multi-line struct declaration with each property on its own line and proper
indentation (e.g., declare struct WindowBounds { let topLeft: NSPoint; let
topRight: NSPoint; let bottomLeft: NSPoint; let bottomRight: NSPoint }) so
property names are easy to scan and maintain, and consider adding a brief
comment or initializer if needed for clarity.
Swift Shift/src/Manager/MouseTracker.swift (1)

120-124: Clarify the key code range intent with accurate documentation.

The range 36...126 does not exclude Escape (53) or represent "Return through Power key" (126 is the Up arrow key). This range actually includes modifiers (Command, Shift, Option, Control), function keys, arrows, and navigation keys—essentially all non-typing keys (letters/numbers are 0-35). The intent appears to be detecting any key press that should pause window tracking, not arbitrary. However, the purpose should be documented in code. Consider adding a comment explaining that the range targets all keys except regular typing keys, or use a named constant with clear intent (e.g., let nonTypingKeyCodes = Set<UInt16>(36...126) // All keys except letter/number typing). Note: ShortcutsManager.swift has a similar range with an inaccurate comment stating "excluding modifiers"—correct that as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Swift` Shift/src/Manager/MouseTracker.swift around lines 120 - 124, The
checkForKeyPresses() function currently uses the opaque range 36...126 which
incorrectly suggests it excludes modifiers and mislabels keys — update the code
to introduce a named constant (e.g., nonTypingKeyCodes or
nonAlphanumericKeyRange) with a clarifying comment that documents the intent
(e.g., "keys intended to pause tracking: non-alphanumeric/navigation/modifier
keys; this range includes Escape, arrows, function and modifier keys") and
replace the magic literal in checkForKeyPresses() with that constant; also fix
the inaccurate comment in ShortcutsManager.swift to reflect that the range
includes modifiers and navigation keys rather than excluding them so future
readers understand the exact behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Swift` Shift/src/Manager/MouseTracker.swift:
- Line 71: The timer closure force-unwraps self (`self!`) which can crash if
MouseTracker is deallocated; update the scheduledTimer callback to safely unwrap
the weak self (e.g., use `guard let self = self else { return }`) and then call
stopTracking(for: self.currentAction), referencing the existing
trackingTimer/trackingTimeout, stopTracking(_:), currentAction and MouseTracker
identifiers.
- Line 20: handleSpaceChange currently forces tracking to resume by doing
isTracking = false; isTracking = true; change it to preserve intentional pauses:
detect whether tracking was actively running before the space change (e.g., save
a previousIsTracking boolean) and only restart tracking if it was running and
not explicitly paused; alternatively, call the existing
pauseTracking()/resumeTracking() helpers and respect any paused flag set by
pauseTracking() so the handler does not override an intentional pause (update
handleSpaceChange to consult pauseTracking()/isPaused or previousIsTracking
instead of unconditionally toggling isTracking).

In `@Swift` Shift/src/Manager/WindowManager.swift:
- Around line 38-43: The getWindow(from:) function force-casts the parent
attribute (p as! AXUIElement) which can crash if kAXParentAttribute returns nil
or an unexpected type; change this to safely unwrap and type-check the parent
(e.g., guard/if let pElem = p as? AXUIElement) before recursing, and return nil
or continue gracefully when the parent is not an AXUIElement so malformed AX
hierarchies cannot crash the app.
- Around line 27-36: getTopWindowAtCursorUsingCGWindowList currently finds a
CoreGraphics window whose bounds contain the cursor but then returns wList.first
from the app's AX windows (kAXWindowsAttribute), which can be the wrong window
for multi-window apps; instead iterate the returned AXUIElement windows (wList)
and for each call AXUIElementCopyAttributeValue for kAXPositionAttribute and
kAXSizeAttribute (or kAXFrame if available), extract AXValue into CGPoint/CGSize
via AXValueGetValue, build a CGRect and check CGRect.contains(mouseLocation) and
return the matching AXUIElement; if none match, fall back to wList.first as
before and keep existing calls to getNSApplication and
PreferencesManager.isAppIgnored to skip ignored apps.

---

Nitpick comments:
In `@Swift` Shift/src/Manager/MouseTracker.swift:
- Around line 120-124: The checkForKeyPresses() function currently uses the
opaque range 36...126 which incorrectly suggests it excludes modifiers and
mislabels keys — update the code to introduce a named constant (e.g.,
nonTypingKeyCodes or nonAlphanumericKeyRange) with a clarifying comment that
documents the intent (e.g., "keys intended to pause tracking:
non-alphanumeric/navigation/modifier keys; this range includes Escape, arrows,
function and modifier keys") and replace the magic literal in
checkForKeyPresses() with that constant; also fix the inaccurate comment in
ShortcutsManager.swift to reflect that the range includes modifiers and
navigation keys rather than excluding them so future readers understand the
exact behavior.

In `@Swift` Shift/src/Manager/WindowManager.swift:
- Line 3: The one-line declaration of the WindowBounds struct (symbol:
WindowBounds with properties topLeft, topRight, bottomLeft, bottomRight) reduces
readability; expand it into a multi-line struct declaration with each property
on its own line and proper indentation (e.g., declare struct WindowBounds { let
topLeft: NSPoint; let topRight: NSPoint; let bottomLeft: NSPoint; let
bottomRight: NSPoint }) so property names are easy to scan and maintain, and
consider adding a brief comment or initializer if needed for clarity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42a3a4e8-c5d7-410d-ad05-dd9253ffc2ff

📥 Commits

Reviewing files that changed from the base of the PR and between ae8cfa9 and 1b62701.

📒 Files selected for processing (2)
  • Swift Shift/src/Manager/MouseTracker.swift
  • Swift Shift/src/Manager/WindowManager.swift

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Swift` Shift/src/Manager/MouseTracker.swift:
- Line 33: forceResetTracking currently only updates initialMouseLocation which
leaves initialWindowLocation and windowSize stale and causes jumps; update
forceResetTracking so that when currentAction != .none you also read the current
window geometry from the tracked window (e.g. use the window/frame accessor used
elsewhere in this file) and assign initialWindowLocation = currentFrame.origin
and windowSize = currentFrame.size (guard/unwrap the tracked window the same way
other methods do) so subsequent deltas are applied against up-to-date baselines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65ebd748-fdbf-47ba-b783-311b48cb482b

📥 Commits

Reviewing files that changed from the base of the PR and between 1b62701 and 3ce7ba2.

📒 Files selected for processing (2)
  • Swift Shift/src/Manager/MouseTracker.swift
  • Swift Shift/src/Manager/WindowManager.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Swift Shift/src/Manager/WindowManager.swift

var r: AnyObject?; AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &r)
if r as? String == kAXWindowRole { return element }
var p: AnyObject?; AXUIElementCopyAttributeValue(element, kAXParentAttribute as CFString, &p)
if let parent = p as? AXUIElement { return getWindow(from: parent) }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think coderabbit was wrong here, I clone it locally and Xcode does not build. Let's change it back:

  if let parent = p as? AXUIElement { ... }

to:

  if let parent = p { return getWindow(from: parent as! AXUIElement) }

Copy link
Copy Markdown
Owner

@pablopunk pablopunk left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! I have not run into CPU issues so I can't really tell if this solves the issue or not, but the solution makes sense to me. I left a small comment; after that, ready to merge

gaorfg-bit pushed a commit to gaorfg-bit/SwiftShift that referenced this pull request Mar 25, 2026
@pablopunk pablopunk merged commit 46350e0 into pablopunk:main Mar 29, 2026
3 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