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.
This commit is contained in:
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 22:39:35 +0100
|
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
|
Inform macOS Zoom of the text cursor position so the zoomed viewport
|
||||||
follows keyboard focus in Emacs.
|
follows keyboard focus in Emacs.
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
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
|
Add the foundation for macOS VoiceOver accessibility in the NS
|
||||||
(Cocoa) port. No existing code paths are modified.
|
(Cocoa) port. No existing code paths are modified.
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
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)
|
protocol)
|
||||||
|
|
||||||
Implement the NSAccessibility text protocol for Emacs buffer windows.
|
Implement the NSAccessibility text protocol for Emacs buffer windows.
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
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
|
element
|
||||||
|
|
||||||
Add VoiceOver notification methods and mode-line readout.
|
Add VoiceOver notification methods and mode-line readout.
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
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.
|
* src/nsterm.m (ns_ax_scan_interactive_spans): New function.
|
||||||
(EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink
|
(EmacsAccessibilityInteractiveSpan): Implement AXButton/AXLink
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
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
|
redisplay
|
||||||
|
|
||||||
Wire the accessibility infrastructure into EmacsView and the
|
Wire the accessibility infrastructure into EmacsView and the
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 12:58:11 +0100
|
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
|
appendix
|
||||||
|
|
||||||
* doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document
|
* doc/emacs/macos.texi (VoiceOver Accessibility): New node. Document
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 14:46:25 +0100
|
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
|
VoiceOver
|
||||||
|
|
||||||
Completion frameworks such as Vertico, Ivy, and Icomplete render
|
Completion frameworks such as Vertico, Ivy, and Icomplete render
|
||||||
|
|||||||
@@ -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 <martin@sukany.cz>
|
From: Martin Sukany <martin@sukany.cz>
|
||||||
Date: Sat, 28 Feb 2026 16:01:29 +0100
|
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
|
VoiceOver
|
||||||
|
|
||||||
Completion frameworks such as Corfu, Company-box, and similar
|
Completion frameworks such as Corfu, Company-box, and similar
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
From 4b739781a019fd1ad5b6ac1c9c12dc62e2c82ec3 Mon Sep 17 00:00:00 2001
|
From 1d67b993dc9072f36a7742fc3d284b7bd935a84d Mon Sep 17 00:00:00 2001
|
||||||
From: Daneel <daneel@sukany.cz>
|
From: Daneel <daneel@sukany.cz>
|
||||||
Date: Sun, 1 Mar 2026 04:56:16 +0100
|
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
|
in ensureTextCache
|
||||||
|
|
||||||
The cache validity check in -[EmacsAccessibilityBuffer ensureTextCache]
|
The cache validity check in -[EmacsAccessibilityBuffer ensureTextCache]
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
From 9e2b56b7aec560540ef9f49c9444c2c0fc903090 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Daneel <daneel@sukany.cz>
|
||||||
|
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
|
||||||
|
|
||||||
Reference in New Issue
Block a user