Skip to content

perf(event-monitor): fix menu bar hover lag on high-frequency mouse events#1118

Merged
tisfeng merged 6 commits intotisfeng:devfrom
MoonMao42:fix/menu-bar-hover-lag
Mar 28, 2026
Merged

perf(event-monitor): fix menu bar hover lag on high-frequency mouse events#1118
tisfeng merged 6 commits intotisfeng:devfrom
MoonMao42:fix/menu-bar-hover-lag

Conversation

@MoonMao42
Copy link
Copy Markdown
Contributor

@MoonMao42 MoonMao42 commented Mar 15, 2026

Summary

Fix #747, #1102

handleMonitorEvent calls NSWorkspace.shared.frontmostApplication on every event including mouseMoved (~60–100 Hz). The original ObjC implementation only fetched it on demand inside specific handlers like leftMouseDown and leftMouseUp.

Changes

  • Return early for mouseMoved and scrollWheel before the frontmostApplication lookup
  • Add 100ms throttle to mouse-moved handling with a visibility guard — isMouseInPopButtonExpandedFrame() now only runs when the pop button is visible
  • Replace pow(x, 2) with x * x in GeometryHelper

Before / After

Before: every mouse-move event triggers a frontmostApplication lookup and pop-button frame check, even when the button is hidden.

After: high-frequency events skip the lookup entirely. The frame check runs at most 10 times per second and only while the button is on screen.

Test plan

  • Hover over the menu bar icon repeatedly — no system lag
  • Keep the app running for 30+ min, verify hover stays smooth
  • Select text via drag / double-click / triple-click — pop button appears and dismisses correctly
  • Scroll while pop button is visible — still dismisses
  • Shortcut translation still works normally

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Hello MKY508, Thank you for your first PR contribution 🎉 MKY508

@tisfeng
Copy link
Copy Markdown
Owner

tisfeng commented Mar 19, 2026

Thanks your PR, we will review it later

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 reduces UI lag in the EventMonitor by avoiding expensive work on high-frequency mouse events and adding throttling around pop-button hover dismissal checks.

Changes:

  • Short-circuit handling for .mouseMoved and .scrollWheel to avoid per-event frontmostApplication lookups.
  • Add a 100ms throttle for mouse-move handling, gated by pop-button visibility.
  • Micro-optimize circle hit-testing by replacing pow(x, 2) with x * x in GeometryHelper.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
Easydict/Swift/Utility/EventMonitor/Core/EventMonitor.swift Skips expensive operations for high-frequency events and throttles mouse-moved hover checks.
Easydict/Swift/Utility/EventMonitor/Utilities/GeometryHelper.swift Replaces pow calls with multiplication for faster distance-squared computation.

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

@Jerry23011
Copy link
Copy Markdown
Collaborator

The mouse movements on menubar were fine for me, even with the current release version.

The movements were fine with this PR. Perhaps we can have someone who was experiencing issues in #747 wants to test?

@MoonMao42
Copy link
Copy Markdown
Contributor Author

MoonMao42 commented Mar 21, 2026

The mouse movements on menubar were fine for me, even with the current release version.

The movements were fine with this PR. Perhaps we can have someone who was experiencing issues in #747 wants to test?

I see that the current release version works fine for you, but I submitted this PR to address issues I've observed in specific scenarios, particularly right after a fresh download and initial installation, or in some other edge cases.
It seems that the lag or performance issue is more likely to be reproduced in those initial-setup environments rather than after the application has been running for a while. This PR is intended to fix the issue described in #747 for everyone, under all conditions.

@tisfeng
Copy link
Copy Markdown
Owner

tisfeng commented Mar 22, 2026

The code looks good, and in theory it can indeed be optimized for performance.

Move the mouse-moved throttle logic into a reusable `ThrottleGate` utility and update `EventMonitor` to use synchronous allow/reset checks.

Split the gate tests into a dedicated suite and register the new source files in the Xcode project.
@tisfeng
Copy link
Copy Markdown
Owner

tisfeng commented Mar 22, 2026

I have fixed this small issue in #1118 (comment) , and refactor the code.

Please review it.

@MoonMao42
Copy link
Copy Markdown
Contributor Author

MoonMao42 commented Mar 22, 2026

@tisfeng Your changes look good. I've pushed some additional fixes on top of them.

I found that EventTapMonitor.start() and stop() are called from different threads — start() runs inside a Task {} in SelectionWorkflow (background thread), while stop() is called from dismissPopButton() on the main thread. They both used CFRunLoopGetCurrent(), which gives different RunLoops on different threads, so CFRunLoopRemoveSource in stop() silently fails and leaks a CFMachPort + CFRunLoopSource every time you select text. I fixed this by switching to CFRunLoopGetMain() and keeping a reference to the installed RunLoop.

I also noticed that a few ObjC view classes (EZQueryView, EZBaseQueryViewController, EZTitlebar, EZTextView) register NotificationCenter observers but never remove them. The EZQueryView one is the worst — the didChangeFontSize block captures self strongly without weakify, so the view never gets deallocated and observers just keep piling up over time. Fixed those too.

I'm not sure these are the full root cause of the reported issues since some users see lag even without frequent text selection, so there might be more to it. I think we can merge this branch as an optimization, but we probably shouldn't close those issues yet until we can confirm the fix actually resolves them.

There are also some smaller fixes in the commit — leaked keyDown monitor, missing cancel before rescheduling delayDismissPopButton, redundant NSEvent.mouseLocation and frontmostApplication calls, and a DEBUG-only slow event handler log.

Fix CGEventTap RunLoop thread mismatch:
- EventTapMonitor.start() was called from a background Swift concurrency
  Task, while stop() was called from the main thread. Both used
  CFRunLoopGetCurrent(), which returns different RunLoops on different
  threads. CFRunLoopRemoveSource in stop() silently failed, leaking a
  CFMachPort + CFRunLoopSource on every text selection cycle.
- Fixed by using CFRunLoopGetMain() and storing the installed RunLoop
  reference so add/remove always target the same RunLoop.

Fix NotificationCenter observer leaks in ObjC view classes:
- EZQueryView: didChangeFontSize block captured self strongly without
  weakify, creating a retain cycle. Observer token was never stored.
- EZBaseQueryViewController: didChangeFontSize block observer token was
  not stored. removeObserver:self in dealloc cannot remove block-based
  observers.
- EZTitlebar: selector-based observers never removed (no dealloc).
- EZTextView: selector-based observers never removed (no dealloc).

Other minor fixes:
- Fix leaked keyDown NSEvent monitor (return value was never stored)
- Cancel previous pending timer before scheduling in delayDismissPopButton
- Pass mouseLocation as parameter to avoid redundant NSEvent.mouseLocation calls
- Remove duplicate frontmostApplication query in handleLeftMouseDown
- Add DEBUG-only slow event handler logging (>16ms warning)
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves menu bar hover responsiveness by reducing work performed on high-frequency input events in the event-monitoring pipeline, and adds a small throttling utility with unit tests.

Changes:

  • Skip frontmostApplication lookup for .mouseMoved / .scrollWheel, and throttle mouse-move handling while the pop button is visible.
  • Introduce ThrottleGate + unit tests to support synchronous throttling.
  • Minor robustness/perf improvements: fix observer lifetime handling in ObjC views/controllers, optimize geometry distance math, and make EventTapMonitor run-loop installation consistent.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
EasydictTests/Utility/ThrottleGateTests.swift Adds unit coverage for the new throttle gate behavior.
Easydict/Swift/Utility/ThrottleGate.swift Introduces a small synchronous throttle primitive used by the event monitor.
Easydict/Swift/Utility/EventMonitor/Core/EventMonitor.swift Early-returns on high-frequency events and throttles .mouseMoved; fixes local monitor removal; resets throttle across visibility sessions.
Easydict/Swift/Utility/EventMonitor/Engine/EventTapMonitor.swift Attempts to make run-loop add/remove consistent by targeting the main run loop.
Easydict/Swift/Utility/EventMonitor/Utilities/GeometryHelper.swift Replaces pow calls with multiplication for faster distance-squared math.
Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m Stores/removes the block-based font-size observer token to avoid leaks.
Easydict/objc/ViewController/View/QueryView/EZQueryView.m Stores/removes the block-based font-size observer token and avoids strong capture.
Easydict/objc/ViewController/View/Titlebar/EZTitlebar.m Removes notification observers in dealloc to prevent callbacks after deallocation.
Easydict/objc/ViewController/View/TextView/EZTextView.m Removes notification observers in dealloc to prevent callbacks after deallocation.
Easydict.xcodeproj/project.pbxproj Registers new Swift source and test files in the Xcode project.

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

tisfeng added 2 commits March 27, 2026 01:14
Add missing `mm_strongify(self)` to the query and result view callbacks
that were still referencing `self` directly after `mm_weakify(self)`.

This restores the intended weak/strong pattern and prevents retain cycles
between the views and their block handlers.
@tisfeng
Copy link
Copy Markdown
Owner

tisfeng commented Mar 28, 2026

@tisfeng Your changes look good. I've pushed some additional fixes on top of them.

I found that EventTapMonitor.start() and stop() are called from different threads — start() runs inside a Task {} in SelectionWorkflow (background thread), while stop() is called from dismissPopButton() on the main thread. They both used CFRunLoopGetCurrent(), which gives different RunLoops on different threads, so CFRunLoopRemoveSource in stop() silently fails and leaks a CFMachPort + CFRunLoopSource every time you select text. I fixed this by switching to CFRunLoopGetMain() and keeping a reference to the installed RunLoop.

I also noticed that a few ObjC view classes (EZQueryView, EZBaseQueryViewController, EZTitlebar, EZTextView) register NotificationCenter observers but never remove them. The EZQueryView one is the worst — the didChangeFontSize block captures self strongly without weakify, so the view never gets deallocated and observers just keep piling up over time. Fixed those too.

I'm not sure these are the full root cause of the reported issues since some users see lag even without frequent text selection, so there might be more to it. I think we can merge this branch as an optimization, but we probably shouldn't close those issues yet until we can confirm the fix actually resolves them.

There are also some smaller fixes in the commit — leaked keyDown monitor, missing cancel before rescheduling delayDismissPopButton, redundant NSEvent.mouseLocation and frontmostApplication calls, and a DEBUG-only slow event handler log.

Good, I reviewed the code, looks fine

Copy link
Copy Markdown
Owner

@tisfeng tisfeng left a comment

Choose a reason for hiding this comment

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

LGTM

@tisfeng tisfeng merged commit 38a85af into tisfeng:dev Mar 28, 2026
4 checks passed
@MoonMao42 MoonMao42 deleted the fix/menu-bar-hover-lag branch March 30, 2026 16:33
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.

🐞 反馈问题:App 运行时间长了,导致交互卡顿

4 participants