patches: fix O(position) lag — O(1) fast path in accessibilityIndexForCharpos:

accessibilityIndexForCharpos: walked composed character sequences
from run.ax_start up to the target charpos offset.  For a run
covering an entire ASCII buffer, chars_in = pt - BUF_BEGV, making
each call O(cursor_position).

This method is called from ensureTextCache on EVERY redisplay frame
(as part of the cache validity check), making each frame O(position)
even when the buffer is completely unchanged.  At line 34,000 of a
large file this is ~1,000,000 iterations per frame.

Fix: when ax_length == length for a run (all single-unit characters),
the ax_index is simply ax_start + chars_in.  O(1) instead of O(N).

This is the symmetric counterpart to the charposForAccessibilityIndex:
fast path added in the previous commit.  Both conversion directions
now run in O(1) for pure-ASCII buffers.
This commit is contained in:
2026-03-01 09:14:52 +01:00
parent fb68dd50ea
commit 0c13f5d6a3
7 changed files with 94 additions and 51 deletions

View File

@@ -1,4 +1,4 @@
From 8dd1a4cd6d3f58a3c6f9454ba1690a442c2048fe Mon Sep 17 00:00:00 2001
From a8172542efe800cf6d29759007c9f826630da881 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 14:46:25 +0100
Subject: [PATCH 7/8] ns: announce overlay completion candidates for VoiceOver
@@ -45,8 +45,8 @@ Key implementation details:
Independent overlay branch, BUF_CHARS_MODIFF gating, candidate
---
src/nsterm.h | 1 +
src/nsterm.m | 332 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 290 insertions(+), 43 deletions(-)
src/nsterm.m | 352 ++++++++++++++++++++++++++++++++++++++++++++-------
2 files changed, 306 insertions(+), 47 deletions(-)
diff --git a/src/nsterm.h b/src/nsterm.h
index 6e830de..2102fb9 100644
@@ -61,7 +61,7 @@ index 6e830de..2102fb9 100644
@property (nonatomic, assign) BOOL cachedMarkActive;
@property (nonatomic, copy) NSString *cachedCompletionAnnouncement;
diff --git a/src/nsterm.m b/src/nsterm.m
index 0a70f3e..c74eaf1 100644
index 51813b5..7ce683d 100644
--- a/src/nsterm.m
+++ b/src/nsterm.m
@@ -7254,11 +7254,154 @@ Accessibility virtual elements (macOS / Cocoa only)
@@ -307,16 +307,43 @@ index 0a70f3e..c74eaf1 100644
NSUInteger lo = 0, hi = visibleRunCount;
while (lo < hi)
{
@@ -8110,7 +8245,7 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
@@ -8109,10 +8244,6 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
lo = mid + 1;
else
{
/* Found: charpos is inside this run. Compute UTF-16 delta
-<<<<<<< Updated upstream
- /* Found: charpos is inside this run. Compute UTF-16 delta
- directly from cachedText — no Lisp calls needed. */
-=======
/* Found: charpos is inside this run. Compute UTF-16 delta.
Fast path for pure-ASCII runs (ax_length == length): every
Emacs charpos maps to exactly one UTF-16 code unit, so the
@@ -8122,7 +8253,23 @@ conversion is O(1). This matters because ensureTextCache
cost per frame even when the buffer is unchanged.
Multi-byte runs fall through to the sequence walk, bounded
by run length (visible window), not total buffer size. */
->>>>>>> Stashed changes
+ NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
+ if (chars_in == 0)
+ return r->ax_start;
+ if (r->ax_length == (NSUInteger) r->length)
+ return r->ax_start + chars_in;
+ if (!cachedText)
+ return r->ax_start;
+ NSUInteger run_end_ax = r->ax_start + r->ax_length;
+ NSUInteger scan = r->ax_start;
+ for (NSUInteger c = 0; c < chars_in && scan < run_end_ax; c++)
+ {
+ NSRange seq = [cachedText
+ rangeOfComposedCharacterSequenceAtIndex:scan];
+ scan = NSMaxRange (seq);
+ }=======
+ directly from cachedText --- no Lisp calls needed. */
+>>>>>>> 8dd1a4c (ns: announce overlay completion candidates for VoiceOver)
NSUInteger chars_in = (NSUInteger)(charpos - r->charpos);
if (chars_in == 0 || !cachedText)
if (chars_in == 0)
return r->ax_start;
@@ -8135,10 +8270,10 @@ - (NSUInteger)accessibilityIndexForCharpos:(ptrdiff_t)charpos
@@ -8151,10 +8298,10 @@ by run length (visible window), not total buffer size. */
/* Convert accessibility string index to buffer charpos.
Safe to call from any thread: uses only cachedText (NSString) and
@@ -329,7 +356,7 @@ index 0a70f3e..c74eaf1 100644
@synchronized (self)
{
if (visibleRunCount == 0)
@@ -8180,7 +8315,7 @@ the slow path (composed character sequence walk), which is
@@ -8196,7 +8343,7 @@ the slow path (composed character sequence walk), which is
return cp;
}
}
@@ -338,7 +365,7 @@ index 0a70f3e..c74eaf1 100644
if (lo > 0)
{
ns_ax_visible_run *last = &visibleRuns[visibleRunCount - 1];
@@ -8202,7 +8337,7 @@ the slow path (composed character sequence walk), which is
@@ -8218,7 +8365,7 @@ the slow path (composed character sequence walk), which is
deadlocking the AX server thread. This is prevented by:
1. validWindow checks WINDOW_LIVE_P and BUFFERP before every
@@ -347,7 +374,7 @@ index 0a70f3e..c74eaf1 100644
2. All dispatch_sync blocks run on the main thread where no
concurrent Lisp code can modify state between checks.
3. block_input prevents timer events and process output from
@@ -8556,6 +8691,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
@@ -8572,6 +8719,50 @@ - (NSInteger)accessibilityInsertionPointLineNumber
return [self lineForAXIndex:point_idx];
}
@@ -398,7 +425,7 @@ index 0a70f3e..c74eaf1 100644
- (NSRange)accessibilityRangeForLine:(NSInteger)line
{
if (![NSThread isMainThread])
@@ -8778,7 +8957,7 @@ - (NSRect)accessibilityFrame
@@ -8794,7 +8985,7 @@ - (NSRect)accessibilityFrame
/* ===================================================================
@@ -407,7 +434,7 @@ index 0a70f3e..c74eaf1 100644
These methods notify VoiceOver of text and selection changes.
Called from the redisplay cycle (postAccessibilityUpdates).
@@ -8793,7 +8972,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8809,7 +9000,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
if (point > self.cachedPoint
&& point - self.cachedPoint == 1)
{
@@ -416,7 +443,7 @@ index 0a70f3e..c74eaf1 100644
[self invalidateTextCache];
[self ensureTextCache];
if (cachedText)
@@ -8812,7 +8991,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
@@ -8828,7 +9019,7 @@ - (void)postTextChangedNotification:(ptrdiff_t)point
/* Update cachedPoint here so the selection-move branch does NOT
fire for point changes caused by edits. WebKit and Chromium
never send both ValueChanged and SelectedTextChanged for the
@@ -425,7 +452,7 @@ index 0a70f3e..c74eaf1 100644
self.cachedPoint = point;
NSDictionary *change = @{
@@ -9145,16 +9324,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
@@ -9161,16 +9352,83 @@ - (void)postAccessibilityNotificationsForFrame:(struct frame *)f
BOOL markActive = !NILP (BVAR (b, mark_active));
/* --- Text changed (edit) --- */
@@ -513,7 +540,7 @@ index 0a70f3e..c74eaf1 100644
{
ptrdiff_t oldPoint = self.cachedPoint;
BOOL oldMarkActive = self.cachedMarkActive;
@@ -9322,7 +9568,7 @@ - (NSRect)accessibilityFrame
@@ -9338,7 +9596,7 @@ - (NSRect)accessibilityFrame
/* ===================================================================
@@ -522,7 +549,7 @@ index 0a70f3e..c74eaf1 100644
=================================================================== */
/* Scan visible range of window W for interactive spans.
@@ -9530,7 +9776,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@@ -9546,7 +9804,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
dispatch_async (dispatch_get_main_queue (), ^{
/* lwin is a Lisp_Object captured by value. This is GC-safe
because Lisp_Objects are tagged integers/pointers that
@@ -531,7 +558,7 @@ index 0a70f3e..c74eaf1 100644
Emacs. The WINDOW_LIVE_P check below guards against the
window being deleted between capture and execution. */
if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin)))
@@ -9556,7 +9802,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@@ -9572,7 +9830,7 @@ - (void) setAccessibilityFocused: (BOOL) focused
@end
@@ -540,7 +567,7 @@ index 0a70f3e..c74eaf1 100644
Methods are kept here (same .m file) so they access the ivars
declared in the @interface ivar block. */
@implementation EmacsAccessibilityBuffer (InteractiveSpans)
@@ -12278,7 +12524,7 @@ - (int) fullscreenState
@@ -12294,7 +12552,7 @@ - (int) fullscreenState
if (WINDOW_LEAF_P (w))
{
@@ -549,7 +576,7 @@ index 0a70f3e..c74eaf1 100644
EmacsAccessibilityBuffer *elem
= [existing objectForKey:[NSValue valueWithPointer:w]];
if (!elem)
@@ -12312,7 +12558,7 @@ - (int) fullscreenState
@@ -12328,7 +12586,7 @@ - (int) fullscreenState
}
else
{
@@ -558,7 +585,7 @@ index 0a70f3e..c74eaf1 100644
Lisp_Object child = w->contents;
while (!NILP (child))
{
@@ -12424,7 +12670,7 @@ - (void)postAccessibilityUpdates
@@ -12440,7 +12698,7 @@ - (void)postAccessibilityUpdates
accessibilityUpdating = YES;
/* Detect window tree change (split, delete, new buffer). Compare
@@ -567,7 +594,7 @@ index 0a70f3e..c74eaf1 100644
Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
if (!EQ (curRoot, lastRootWindow))
{
@@ -12433,12 +12679,12 @@ - (void)postAccessibilityUpdates
@@ -12449,12 +12707,12 @@ - (void)postAccessibilityUpdates
}
/* If tree is stale, rebuild FIRST so we don't iterate freed