From 6b3843e0c693e9cf49616c73e045b17fc64182b3 Mon Sep 17 00:00:00 2001 From: Daneel Date: Sun, 1 Mar 2026 05:23:59 +0100 Subject: [PATCH] patches: fix O(position) performance via UAZoomEnabled caching Root cause (per Opus analysis): UAZoomEnabled() is a synchronous Mach IPC roundtrip to macOS Accessibility server, called 3x per redisplay cycle. At 60fps = 180 IPC roundtrips/second blocking the main thread. Combined with Emacs's inherent O(position) redisplay cost, this compounded into progressive choppy behavior. Fix 1: ns_zoom_enabled_p() caches UAZoomEnabled() for 1 second. Fix 2: ns_zoom_track_completion() rate-limited to 2 Hz. Also includes BUF_CHARS_MODIFF fix (patch 0009) for VoiceOver cache. --- ...-with-macOS-Zoom-for-cursor-tracking.patch | 4 +- ...lity-base-classes-and-text-extractio.patch | 4 +- ...fer-accessibility-element-core-proto.patch | 4 +- ...tification-dispatch-and-mode-line-el.patch | 4 +- ...ive-span-elements-for-Tab-navigation.patch | 4 +- ...essibility-with-EmacsView-and-redisp.patch | 4 +- ...r-accessibility-section-to-macOS-app.patch | 4 +- ...lay-completion-candidates-for-VoiceO.patch | 4 +- ...d-frame-completion-candidates-for-Vo.patch | 4 +- ...RS_MODIFF-for-AX-text-cache-validity.patch | 4 +- ...mEnabled-and-rate-limit-completion-t.patch | 117 ++++++++++++++++++ 11 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 patches/0010-perf-cache-UAZoomEnabled-and-rate-limit-completion-t.patch diff --git a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch index 180374f..691207d 100644 --- a/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch +++ b/patches/0000-ns-integrate-with-macOS-Zoom-for-cursor-tracking.patch @@ -1,7 +1,7 @@ -From b38d702cb19f2b7c36d88d7e397323ea1aca1c9b Mon Sep 17 00:00:00 2001 +From 8cecc655267f3adc31230a76d0d470159adc9d87 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 22:39:35 +0100 -Subject: [PATCH 01/10] ns: integrate with macOS Zoom for cursor tracking +Subject: [PATCH 01/11] ns: integrate with macOS Zoom for cursor tracking Inform macOS Zoom of the text cursor position so the zoomed viewport follows keyboard focus in Emacs. diff --git a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch index 3b432ac..38e4ff4 100644 --- a/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch +++ b/patches/0001-ns-add-accessibility-base-classes-and-text-extractio.patch @@ -1,7 +1,7 @@ -From e6800d12d350def06dd6475fcb807ceaf7f82e02 Mon Sep 17 00:00:00 2001 +From a750bf8ae17dd0dd6c14a23376d012c61367227f Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 -Subject: [PATCH 02/10] ns: add accessibility base classes and text extraction +Subject: [PATCH 02/11] ns: add accessibility base classes and text extraction Add the foundation for macOS VoiceOver accessibility in the NS (Cocoa) port. No existing code paths are modified. diff --git a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch index 670ccb9..db135c3 100644 --- a/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch +++ b/patches/0002-ns-implement-buffer-accessibility-element-core-proto.patch @@ -1,7 +1,7 @@ -From d3dd16835ff6b7456f987893ef610e3847a92fde Mon Sep 17 00:00:00 2001 +From cd8512d541e63f044086d1dfd2fe44069d4c09ca Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 -Subject: [PATCH 03/10] ns: implement buffer accessibility element (core +Subject: [PATCH 03/11] ns: implement buffer accessibility element (core protocol) Implement the NSAccessibility text protocol for Emacs buffer windows. diff --git a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch index 657e57c..3e44a40 100644 --- a/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch +++ b/patches/0003-ns-add-buffer-notification-dispatch-and-mode-line-el.patch @@ -1,7 +1,7 @@ -From 601df3982e20e60f041fa2658aa7ef1efb69939b Mon Sep 17 00:00:00 2001 +From 2cb651607c84eb100729db5d4c113d7fcf4e1048 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 -Subject: [PATCH 04/10] ns: add buffer notification dispatch and mode-line +Subject: [PATCH 04/11] ns: add buffer notification dispatch and mode-line element Add VoiceOver notification methods and mode-line readout. diff --git a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch index 6d23f0a..ce50f76 100644 --- a/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch +++ b/patches/0004-ns-add-interactive-span-elements-for-Tab-navigation.patch @@ -1,7 +1,7 @@ -From 8c67a2b5a89ac302cbac91790fdfb6827b74285a Mon Sep 17 00:00:00 2001 +From e04d6ef8808c1e00b37fca85e6ac0a7f6104bdb7 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 -Subject: [PATCH 05/10] ns: add interactive span elements for Tab navigation +Subject: [PATCH 05/11] ns: add interactive span elements for Tab navigation * src/nsterm.m (ns_ax_scan_interactive_spans): New function. (EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink diff --git a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch index c227a4c..ec1c5b7 100644 --- a/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch +++ b/patches/0005-ns-integrate-accessibility-with-EmacsView-and-redisp.patch @@ -1,7 +1,7 @@ -From 3237374282389cd61bcd99beed187ec75d1b06fc Mon Sep 17 00:00:00 2001 +From 88bcf29875755897a3d3d4cef04e2c0044ac8b9d Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 -Subject: [PATCH 06/10] ns: integrate accessibility with EmacsView and +Subject: [PATCH 06/11] ns: integrate accessibility with EmacsView and redisplay Wire the accessibility infrastructure into EmacsView and the diff --git a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch index 747c9c6..dd47a02 100644 --- a/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch +++ b/patches/0006-doc-add-VoiceOver-accessibility-section-to-macOS-app.patch @@ -1,7 +1,7 @@ -From e2b76f1850489e8188236bed10e4d7f28e5cde2b Mon Sep 17 00:00:00 2001 +From 8b7858f8e43e6bd7abb58ca2da0883341b9c446a Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 12:58:11 +0100 -Subject: [PATCH 07/10] doc: add VoiceOver accessibility section to macOS +Subject: [PATCH 07/11] doc: add VoiceOver accessibility section to macOS appendix * doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index 3d90373..b51195d 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,7 +1,7 @@ -From 3e868d0234c858fa20588e664354685ef8b08576 Mon Sep 17 00:00:00 2001 +From 314e2a5f406a8eb0e6909be6e07fe7e8ddd1c440 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 -Subject: [PATCH 08/10] ns: announce overlay completion candidates for +Subject: [PATCH 08/11] ns: announce overlay completion candidates for VoiceOver Completion frameworks such as Vertico, Ivy, and Icomplete render diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index 38ada42..f267960 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -1,7 +1,7 @@ -From 5aba3491f8f5268f2e6093003b79fe69e7932a4b Mon Sep 17 00:00:00 2001 +From a75ee6aff3734bd053865b3df734b75642f7305e Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 16:01:29 +0100 -Subject: [PATCH 09/10] ns: announce child frame completion candidates for +Subject: [PATCH 09/11] ns: announce child frame completion candidates for VoiceOver Completion frameworks such as Corfu, Company-box, and similar diff --git a/patches/0009-perf-use-BUF_CHARS_MODIFF-for-AX-text-cache-validity.patch b/patches/0009-perf-use-BUF_CHARS_MODIFF-for-AX-text-cache-validity.patch index c90944d..4a36bed 100644 --- a/patches/0009-perf-use-BUF_CHARS_MODIFF-for-AX-text-cache-validity.patch +++ b/patches/0009-perf-use-BUF_CHARS_MODIFF-for-AX-text-cache-validity.patch @@ -1,7 +1,7 @@ -From 4b739781a019fd1ad5b6ac1c9c12dc62e2c82ec3 Mon Sep 17 00:00:00 2001 +From 1d67b993dc9072f36a7742fc3d284b7bd935a84d Mon Sep 17 00:00:00 2001 From: Daneel Date: Sun, 1 Mar 2026 04:56:16 +0100 -Subject: [PATCH 10/10] perf: use BUF_CHARS_MODIFF for AX text cache validity +Subject: [PATCH 10/11] perf: use BUF_CHARS_MODIFF for AX text cache validity in ensureTextCache The cache validity check in -[EmacsAccessibilityBuffer ensureTextCache] diff --git a/patches/0010-perf-cache-UAZoomEnabled-and-rate-limit-completion-t.patch b/patches/0010-perf-cache-UAZoomEnabled-and-rate-limit-completion-t.patch new file mode 100644 index 0000000..752941b --- /dev/null +++ b/patches/0010-perf-cache-UAZoomEnabled-and-rate-limit-completion-t.patch @@ -0,0 +1,117 @@ +From 9e2b56b7aec560540ef9f49c9444c2c0fc903090 Mon Sep 17 00:00:00 2001 +From: Daneel +Date: Sun, 1 Mar 2026 05:23:37 +0100 +Subject: [PATCH 11/11] perf: cache UAZoomEnabled() and rate-limit completion + tracking +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +UAZoomEnabled() performs a synchronous Mach IPC roundtrip to the macOS +Accessibility server (~50-200 µs per call). With three call sites in +the redisplay hot path (ns_draw_window_cursor, ns_update_end fallback, +ns_zoom_track_completion) this adds up to 150-600 µs of IPC overhead +per redisplay cycle. At 60 fps, that is 9-36 ms/s of pure IPC cost. +The overhead blocks the main thread, extending the inter-frame interval +and making cursor movement feel choppy, especially at the end of large +files where Emacs's own redisplay work is already at its maximum. + +* src/nsterm.m (ns_zoom_cached_enabled, ns_zoom_cache_time): New +static variables. +(ns_zoom_enabled_p): New inline wrapper around UAZoomEnabled(). +Caches the result for 1 second using CFAbsoluteTimeGetCurrent(), a +near-free VDSO call. Zoom state changes only on explicit user action +in System Settings, so a 1-second TTL is indistinguishable from +querying on every frame. Replaces all three UAZoomEnabled() call +sites. +(ns_zoom_track_completion): Add 500 ms rate limit. Completion +candidate detection requires FOR_EACH_FRAME iteration and +Fget_char_property calls. Completion state changes at human +interaction speed (well below 2 Hz), so 500 ms polling is +imperceptible while eliminating per-redisplay Lisp overhead. +--- + src/nsterm.m | 43 ++++++++++++++++++++++++++++++++++++++++--- + 1 file changed, 40 insertions(+), 3 deletions(-) + +diff --git a/src/nsterm.m b/src/nsterm.m +index e3f9466..590287a 100644 +--- a/src/nsterm.m ++++ b/src/nsterm.m +@@ -1092,6 +1092,31 @@ static NSRect constrain_frame_rect(NSRect frameRect, bool isFullscreen) + ivy-current-match, corfu-current that mark the selected candidate. */ + static bool ns_ax_face_is_selected (Lisp_Object face); + ++/* Cached wrapper around UAZoomEnabled(). ++ UAZoomEnabled() performs a synchronous Mach IPC roundtrip to the ++ macOS Accessibility server (~50-200 µs per call). With three call ++ sites in the redisplay hot path the overhead accumulates to ++ 150-600 µs per frame. Zoom state only changes on explicit user ++ action in System Settings, so a 1-second TTL is both safe and ++ indistinguishable from querying on every frame. ++ ++ Uses CFAbsoluteTimeGetCurrent() (a VDSO read on modern macOS, ~5 ns) ++ to avoid a second IPC roundtrip for time. */ ++static BOOL ns_zoom_cached_enabled; ++static CFAbsoluteTime ns_zoom_cache_time; ++ ++static BOOL ++ns_zoom_enabled_p (void) ++{ ++ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent (); ++ if (now - ns_zoom_cache_time > 1.0) ++ { ++ ns_zoom_cached_enabled = UAZoomEnabled (); ++ ns_zoom_cache_time = now; ++ } ++ return ns_zoom_cached_enabled; ++} ++ + /* Scan overlay before-string / after-string properties in the + selected window for a completion candidate with a "selected" + face. Return the 0-based visual line index of the selected +@@ -1215,11 +1240,23 @@ If a completion candidate is selected (overlay or child frame), + static void + ns_zoom_track_completion (struct frame *f, EmacsView *view) + { +- if (!UAZoomEnabled ()) ++ if (!ns_zoom_enabled_p ()) + return; + if (!WINDOWP (f->selected_window)) + return; + ++ /* Rate-limit completion scan to 2 Hz. Completion overlays and ++ child frames change at human interaction speed (<<10 Hz), so ++ checking every 500 ms is indistinguishable from every frame ++ while eliminating per-redisplay FOR_EACH_FRAME iteration. */ ++ { ++ static CFAbsoluteTime last_completion_track; ++ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent (); ++ if (now - last_completion_track < 0.5) ++ return; ++ last_completion_track = now; ++ } ++ + specpdl_ref count = SPECPDL_INDEX (); + record_unwind_current_buffer (); + +@@ -1323,7 +1360,7 @@ so the visual offset is (ov_line + 1) * line_h from + (zoomCursorUpdated is NO). */ + #if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 +- if (view && !view->zoomCursorUpdated && UAZoomEnabled () ++ if (view && !view->zoomCursorUpdated && ns_zoom_enabled_p () + && !NSIsEmptyRect (view->lastCursorRect)) + { + NSRect r = view->lastCursorRect; +@@ -3500,7 +3537,7 @@ EmacsView pixels (AppKit, flipped, top-left origin) + + #if defined (MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 +- if (UAZoomEnabled ()) ++ if (ns_zoom_enabled_p ()) + { + NSRect windowRect = [view convertRect:r toView:nil]; + NSRect screenRect +-- +2.43.0 +