patches: update README — document async notification posting

Add deadlock prevention section to THREADING MODEL, note async
posting in NOTIFICATION STRATEGY, add design decision 6a, and
add deadlock regression test case (#24) to testing checklist.
This commit is contained in:
2026-02-27 15:44:22 +01:00
parent 111013ddf1
commit 60e9ea2c59

View File

@@ -3,7 +3,7 @@ EMACS NS VOICEOVER ACCESSIBILITY PATCH
patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch
author: Martin Sukany <martin@sukany.cz> author: Martin Sukany <martin@sukany.cz>
files: src/nsterm.h (+108 lines) files: src/nsterm.h (+108 lines)
src/nsterm.m (+2561 ins, -140 del, +2421 net) src/nsterm.m (+2588 ins, -140 del, +2448 net)
OVERVIEW OVERVIEW
@@ -159,6 +159,30 @@ THREADING MODEL
return result; return result;
} }
Async notification posting (deadlock prevention):
NSAccessibilityPostNotification may synchronously invoke VoiceOver
callbacks from a private AX server thread. Those callbacks call
AX getters which dispatch_sync back to the main queue. If the
main thread is still inside the notification-posting method (e.g.,
postAccessibilityUpdates called from ns_update_end), the
dispatch_sync deadlocks: the main thread waits for VoiceOver to
finish processing the notification, while VoiceOver's thread waits
for the main queue to become available.
To break this cycle, all notification posting goes through two
static inline wrappers:
ns_ax_post_notification(element, name)
ns_ax_post_notification_with_info(element, name, info)
These wrappers defer the actual NSAccessibilityPostNotification
call via dispatch_async(dispatch_get_main_queue(), ^{ ... }).
The current method returns first, freeing the main queue, so
VoiceOver's dispatch_sync calls can proceed without deadlock.
Block captures retain ObjC objects (element, info dictionary)
for the lifetime of the deferred block.
Cached data written on main thread and read from any thread: Cached data written on main thread and read from any thread:
- cachedText (NSString *): written by ensureTextCache on main. - cachedText (NSString *): written by ensureTextCache on main.
- visibleRuns (ns_ax_visible_run *): written by ensureTextCache. - visibleRuns (ns_ax_visible_run *): written by ensureTextCache.
@@ -171,7 +195,11 @@ THREADING MODEL
NOTIFICATION STRATEGY NOTIFICATION STRATEGY
--------------------- ---------------------
Notifications are posted from -postAccessibilityNotificationsForFrame: All notifications are posted asynchronously via
ns_ax_post_notification / ns_ax_post_notification_with_info
(dispatch_async wrappers -- see THREADING MODEL for rationale).
Notifications are generated by -postAccessibilityNotificationsForFrame:
which runs on the main thread after every redisplay cycle. The which runs on the main thread after every redisplay cycle. The
method detects three mutually exclusive events: method detects three mutually exclusive events:
@@ -443,6 +471,18 @@ KEY DESIGN DECISIONS
calls ns_update_end -> postAccessibilityUpdates. The BOOL flag calls ns_update_end -> postAccessibilityUpdates. The BOOL flag
breaks this recursion. breaks this recursion.
6a. Async notification posting (dispatch_async wrappers).
NSAccessibilityPostNotification can synchronously trigger
VoiceOver queries from a background AX server thread. Those
queries dispatch_sync to the main queue. If the main thread
is still inside postAccessibilityUpdates (or windowDidBecomeKey,
or setAccessibilityFocused:), the dispatch_sync deadlocks.
All 14 notification sites use ns_ax_post_notification / _with_info
wrappers that defer posting via dispatch_async, freeing the main
queue before VoiceOver's callbacks arrive. This follows the same
pattern used by WebKit's AXObjectCacheMac (deferred posting via
performSelector:withObject:afterDelay:0).
7. lispWindow (Lisp_Object) instead of raw struct window *. 7. lispWindow (Lisp_Object) instead of raw struct window *.
struct window pointers can become dangling after delete-window. struct window pointers can become dangling after delete-window.
Storing the Lisp_Object and using WINDOW_LIVE_P + XWINDOW at the Storing the Lisp_Object and using WINDOW_LIVE_P + XWINDOW at the
@@ -554,10 +594,15 @@ TESTING CHECKLIST
22. Delete a window with C-x 0. No crash should occur. 22. Delete a window with C-x 0. No crash should occur.
23. Switch buffers with C-x b. VoiceOver should read new buffer. 23. Switch buffers with C-x b. VoiceOver should read new buffer.
Deadlock regression (async notifications):
24. With VoiceOver on: M-x, type partial command, M-v to
*Completions*, Tab to a candidate, Enter to execute, then
C-x o to switch windows. Emacs must not hang.
Stress test: Stress test:
24. Open a large file (>5000 lines). Navigate with C-v / M-v. 26. Open a large file (>5000 lines). Navigate with C-v / M-v.
Verify no significant lag in VoiceOver speech response. Verify no significant lag in VoiceOver speech response.
25. Open an org-mode file with many folded sections. Verify that 27. Open an org-mode file with many folded sections. Verify that
folded (invisible) text is not announced during navigation. folded (invisible) text is not announced during navigation.
-- end of README -- -- end of README --