Bug 262433

Summary: AX: Move AXSelectedTextMarkerRange off of the main thread
Product: WebKit Reporter: Joshua Hoffman <jhoffman23>
Component: AccessibilityAssignee: Joshua Hoffman <jhoffman23>
Status: RESOLVED FIXED    
Severity: Normal CC: aboxhall, andresg_22, apinheiro, cfleizach, dmazzoni, ews-watchlist, jcraig, jdiggs, samuel_white, webkit-bug-importer
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
Attachments:
Description Flags
Patch
none
Patch
none
Patch none

Description Joshua Hoffman 2023-09-29 22:12:47 PDT
In isolated tree mode, our implementation currently has to go to the main thread to retrieve AXSelectedTextMarkerRange. This should be cached and served off of the secondary AX thread instead.
Comment 1 Radar WebKit Bug Importer 2023-09-29 22:12:54 PDT
<rdar://problem/116271179>
Comment 2 Joshua Hoffman 2023-09-29 22:13:56 PDT
rdar://116128660
Comment 3 Joshua Hoffman 2023-09-29 22:30:45 PDT
Created attachment 467992 [details]
Patch
Comment 4 chris fleizach 2023-09-30 00:04:59 PDT
Comment on attachment 467992 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=467992&action=review

> COMMIT_MESSAGE:12
> +From there, a new method, onSelectedTextChanged, handles updating the isolated tree's

do we have a way to cache this data if accessibility was not enabled initially?

> Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp:1009
> +    fprintf(stderr, "JDH: selectedTextMarkerRange()\n");

remove
Comment 5 Joshua Hoffman 2023-09-30 08:31:32 PDT
Comment on attachment 467992 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=467992&action=review

>> COMMIT_MESSAGE:12
>> +From there, a new method, onSelectedTextChanged, handles updating the isolated tree's
> 
> do we have a way to cache this data if accessibility was not enabled initially?

Good point—when we create an isolated tree we should just be able to grab the visible selection like we do in the live object? I can add that to this patch.
Comment 6 Andres Gonzalez 2023-10-02 11:35:15 PDT
(In reply to Joshua Hoffman from comment #3)
> Created attachment 467992 [details]
> Patch
When text selection changes, the AXObjectCache is notified through a call to
postTextStateChangeNotification from FrameSelection::notifyAccessibilityForSelectionChange.

AG: is this also true when the selection changes happens in non-text objects like images and buttons?

From there, a new method, onSelectedTextChanged, handles updating the isolated tree's
m_selectedTextMarkerRange with the relevant text markers for the start and end of the
selection. These are stored in a pair of TextMarkerData structs.

An important piece of this is that we need the nodes of the start and end text marker

AG: Typo: start and end marker -> start and end markers

to exist in the isolated tree. So, if one or both of these nodes are ignored, they are added
to the isolated tree as unconnected nodes. This is handled in onSelectedTextChanged.

Finally, when the range is requested via AXIsolatedObject::selectedTextMarkerRange(),
an AXTextMarkerRange is constructed from the cached TextMarkerData pair.

This patch also modifies previous tests to make them asynchronous for this new behavior.

* LayoutTests/accessibility/mac/doctype-node-in-text-marker-crash-expected.txt:
* LayoutTests/accessibility/mac/doctype-node-in-text-marker-crash.html:
* LayoutTests/accessibility/mac/replace-text-with-range-expected.txt:
* LayoutTests/accessibility/mac/replace-text-with-range-value-change-notification.html:
* LayoutTests/accessibility/mac/replace-text-with-range.html:
* LayoutTests/accessibility/mac/text-marker-p-tags-expected.txt:
* LayoutTests/accessibility/mac/text-marker-p-tags.html:
* Source/WebCore/accessibility/AXCoreObject.h:
* Source/WebCore/accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::postTextStateChangeNotification):
* Source/WebCore/accessibility/AXObjectCache.h:
* Source/WebCore/accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::selectedTextMarkerRange):
* Source/WebCore/accessibility/AccessibilityObject.h:
(WebCore::AccessibilityObject::selectedVisiblePositionRange const):
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp:
(WebCore::AXIsolatedObject::selectedTextMarkerRange):
(WebCore::AXIsolatedObject::selectedVisiblePositionRange const): Deleted.
* Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h:
* Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h:
(WebCore::AXIsolatedTree::selectedTextMarkerRange):
(WebCore::AXIsolatedTree::setSelectedTextMarkerRange):
* Source/WebCore/accessibility/mac/AXObjectCacheMac.mm:
(WebCore::AXObjectCache::onSelectedTextChanged):
* Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
(-[WebAccessibilityObjectWrapper selectedTextMarkerRange]):
---
 Source/WebCore/accessibility/AXCoreObject.h   |  4 +-
 .../WebCore/accessibility/AXObjectCache.cpp   |  3 +
 Source/WebCore/accessibility/AXObjectCache.h  |  3 +
 .../accessibility/AccessibilityObject.cpp     | 10 ++
 .../accessibility/AccessibilityObject.h       |  6 +-
 .../isolatedtree/AXIsolatedObject.cpp         | 18 ++--
 .../isolatedtree/AXIsolatedObject.h           |  4 +-
 .../isolatedtree/AXIsolatedTree.h             |  5 +
 .../accessibility/mac/AXObjectCacheMac.mm     | 24 +++++
 .../mac/WebAccessibilityObjectWrapperMac.mm   | 17 ++--
 ...ype-node-in-text-marker-crash-expected.txt |  9 +-
 .../doctype-node-in-text-marker-crash.html    | 95 ++++++++++---------
 .../mac/replace-text-with-range-expected.txt  | 18 ++--
 ...-with-range-value-change-notification.html | 59 +++++++-----
 .../mac/replace-text-with-range.html          | 42 ++++----
 .../mac/text-marker-p-tags-expected.txt       |  6 +-
 .../accessibility/mac/text-marker-p-tags.html | 39 +++++---
 17 files changed, 222 insertions(+), 140 deletions(-)

diff --git a/Source/WebCore/accessibility/AXCoreObject.h b/Source/WebCore/accessibility/AXCoreObject.h
index 0c2df24ba1b2..c51043b0e158 100644
--- a/Source/WebCore/accessibility/AXCoreObject.h
+++ b/Source/WebCore/accessibility/AXCoreObject.h
@@ -1277,12 +1277,14 @@ public:
     virtual VisiblePositionRange styleRangeForPosition(const VisiblePosition&) const = 0;
     virtual VisiblePositionRange visiblePositionRangeForRange(const CharacterRange&) const = 0;
     virtual VisiblePositionRange lineRangeForPosition(const VisiblePosition&) const = 0;
-    virtual VisiblePositionRange selectedVisiblePositionRange() const = 0;

     virtual std::optional<SimpleRange> rangeForCharacterRange(const CharacterRange&) const = 0;
 #if PLATFORM(COCOA)
     virtual AXTextMarkerRange textMarkerRangeForNSRange(const NSRange&) const = 0;
 #endif
+#if PLATFORM(MAC)
+    virtual AXTextMarkerRange selectedTextMarkerRange() = 0;
+#endif

AG: don't we need this in other platforms? It should work the same.

     virtual String stringForRange(const SimpleRange&) const = 0;
     virtual IntRect boundsForRange(const SimpleRange&) const = 0;
diff --git a/Source/WebCore/accessibility/AXObjectCache.cpp b/Source/WebCore/accessibility/AXObjectCache.cpp
index 1922e7bbd960..ae78e92330b8 100644
--- a/Source/WebCore/accessibility/AXObjectCache.cpp
+++ b/Source/WebCore/accessibility/AXObjectCache.cpp
@@ -1983,6 +1983,9 @@ void AXObjectCache::postTextStateChangeNotification(AccessibilityObject* object,
         const AXTextStateChangeIntent& newIntent = (intent.type == AXTextStateChangeTypeUnknown || (m_isSynchronizingSelection && m_textSelectionIntent.type != AXTextStateChangeTypeUnknown)) ? m_textSelectionIntent : intent;
         postTextStateChangePlatformNotification(object, newIntent, selection);
     }
+#if PLATFORM(MAC)
+    onSelectedTextChanged(selection);
+#endif

AG: I think it should be the same for iOS and ATSPI.

 #else
     UNUSED_PARAM(object);
     UNUSED_PARAM(intent);
diff --git a/Source/WebCore/accessibility/AXObjectCache.h b/Source/WebCore/accessibility/AXObjectCache.h
index c26ee022b20c..1abe4a313e63 100644
--- a/Source/WebCore/accessibility/AXObjectCache.h
+++ b/Source/WebCore/accessibility/AXObjectCache.h
@@ -211,6 +211,9 @@ public:
     // an AXNodeObject. This occurs when an Element with no renderer is
     // re-parented into a subtree that does have a renderer.
     void onRendererCreated(Element&);
+#if PLATFORM(MAC)
+    void onSelectedTextChanged(const VisiblePositionRange&);
+#endif

AG: same here.

     void updateLoadingProgress(double);
     void loadingFinished() { updateLoadingProgress(1); }
diff --git a/Source/WebCore/accessibility/AccessibilityObject.cpp b/Source/WebCore/accessibility/AccessibilityObject.cpp
index 7d0f55b40908..982bfd4e022e 100644
--- a/Source/WebCore/accessibility/AccessibilityObject.cpp
+++ b/Source/WebCore/accessibility/AccessibilityObject.cpp
@@ -1665,6 +1665,16 @@ VisiblePositionRange AccessibilityObject::lineRangeForPosition(const VisiblePosi
     return { startOfLine(visiblePosition), endOfLine(visiblePosition) };
 }

+#if PLATFORM(MAC)
+AXTextMarkerRange AccessibilityObject::selectedTextMarkerRange()
+{
+    auto visibleRange = selectedVisiblePositionRange();
+    if (visibleRange.isNull())
+        return nil;

AG: return { }; i.e., the AXTextMarkerRange object created with the default constructor.

+    return AXTextMarkerRange(visibleRange);

AG: you should be able to write just:

    return { visibleRange };

No need for the if statement.

+}
+#endif
+
 bool AccessibilityObject::replacedNodeNeedsCharacter(Node* replacedNode)
 {
     // we should always be given a rendered node and a replaced node, but be safe
diff --git a/Source/WebCore/accessibility/AccessibilityObject.h b/Source/WebCore/accessibility/AccessibilityObject.h
index d29dbcbb23d8..56cb4a7ff952 100644
--- a/Source/WebCore/accessibility/AccessibilityObject.h
+++ b/Source/WebCore/accessibility/AccessibilityObject.h
@@ -552,13 +552,15 @@ public:
     VisiblePositionRange styleRangeForPosition(const VisiblePosition&) const override;
     VisiblePositionRange visiblePositionRangeForRange(const CharacterRange&) const override;
     VisiblePositionRange lineRangeForPosition(const VisiblePosition&) const override;
-    VisiblePositionRange selectedVisiblePositionRange() const override { return { }; }
+    virtual VisiblePositionRange selectedVisiblePositionRange() const { return { }; }

     std::optional<SimpleRange> rangeForCharacterRange(const CharacterRange&) const override;
 #if PLATFORM(COCOA)
     AXTextMarkerRange textMarkerRangeForNSRange(const NSRange&) const override;
 #endif
-
+#if PLATFORM(MAC)
+    AXTextMarkerRange selectedTextMarkerRange() override;
+#endif

AG: should be the same in other platforms.

     static String stringForVisiblePositionRange(const VisiblePositionRange&);
     String stringForRange(const SimpleRange&) const override;
     virtual IntRect boundsForVisiblePositionRange(const VisiblePositionRange&) const { return IntRect(); }
diff --git a/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp b/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp
index 90d86087e862..57cc5b9e981a 100644
--- a/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp
+++ b/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.cpp
@@ -1003,6 +1003,15 @@ std::optional<SimpleRange> AXIsolatedObject::rangeForCharacterRange(const Charac
     return axObject ? axObject->rangeForCharacterRange(axRange) : std::nullopt;
 }

+#if PLATFORM(MAC)

AG: same for other platforms.

+AXTextMarkerRange AXIsolatedObject::selectedTextMarkerRange()
+{
+    fprintf(stderr, "JDH: selectedTextMarkerRange()\n");

AG: debugging leftover.

+    auto textMarkerPair = tree()->selectedTextMarkerRange();
+    return { textMarkerPair.first, textMarkerPair.second };
+}
+#endif
+
 String AXIsolatedObject::stringForRange(const SimpleRange& range) const
 {
     ASSERT(isMainThread());
@@ -1367,15 +1376,6 @@ VisibleSelection AXIsolatedObject::selection() const
     return object ? object->selection() : VisibleSelection();
 }

-VisiblePositionRange AXIsolatedObject::selectedVisiblePositionRange() const
-{
-    ASSERT(isMainThread());
-
-    if (auto* axObject = associatedAXObject())
-        return axObject->selectedVisiblePositionRange();
-    return { };
-}
-
 void AXIsolatedObject::setSelectedVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const
 {
     ASSERT(isMainThread());
diff --git a/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h b/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h
index 47d3e7c6d7c4..4666a253a78f 100644
--- a/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h
+++ b/Source/WebCore/accessibility/isolatedtree/AXIsolatedObject.h
@@ -353,7 +353,6 @@ private:
     unsigned doAXLineForIndex(unsigned) override;

     VisibleSelection selection() const override;
-    VisiblePositionRange selectedVisiblePositionRange() const override;
     void setSelectedVisiblePositionRange(const VisiblePositionRange&) const override;

     std::optional<SimpleRange> simpleRange() const override;
@@ -376,6 +375,9 @@ private:
     std::optional<SimpleRange> rangeForCharacterRange(const CharacterRange&) const override;
 #if PLATFORM(COCOA)
     AXTextMarkerRange textMarkerRangeForNSRange(const NSRange&) const override;
+#endif
+#if PLATFORM(MAC)

AG: same for other platforms.

+    AXTextMarkerRange selectedTextMarkerRange() override;
 #endif
     String stringForRange(const SimpleRange&) const override;
     IntRect boundsForRange(const SimpleRange&) const override;
diff --git a/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h b/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h
index 3d91e290da4e..40be74a9d159 100644
--- a/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h
+++ b/Source/WebCore/accessibility/isolatedtree/AXIsolatedTree.h
@@ -28,6 +28,7 @@
 #if ENABLE(ACCESSIBILITY_ISOLATED_TREE)

 #include "AXCoreObject.h"
+#include "AXTextMarker.h"
 #include "AXTreeStore.h"
 #include "PageIdentifier.h"
 #include "RenderStyleConstants.h"
@@ -327,6 +328,9 @@ public:
     // Use only if the s_storeLock is already held like in findAXTree.
     WEBCORE_EXPORT OptionSet<ActivityState> lockedPageActivityState() const;

+    std::pair<TextMarkerData, TextMarkerData> selectedTextMarkerRange() { return m_selectedTextMarkerRange; };

AG: Don't need a pair, return an AXTextMarkerRange.

+    void setSelectedTextMarkerRange(std::pair<TextMarkerData, TextMarkerData> data) { m_selectedTextMarkerRange = data; }

AG: don't need a pair, pass an AXTextMarkerRange.

+
 private:
     AXIsolatedTree(AXObjectCache&);
     static void storeTree(AXObjectCache&, const Ref<AXIsolatedTree>&);
@@ -409,6 +413,7 @@ private:
     std::atomic<bool> m_relationsNeedUpdate { true };

     Lock m_changeLogLock;
+    std::pair<TextMarkerData, TextMarkerData> m_selectedTextMarkerRange { };

AG: AXTextMarkerRange m_selectedTextMarkerRange;
 };

 inline AXObjectCache* AXIsolatedTree::axObjectCache() const
diff --git a/Source/WebCore/accessibility/mac/AXObjectCacheMac.mm b/Source/WebCore/accessibility/mac/AXObjectCacheMac.mm
index b1cfe1b0f48b..3fa2ac38ffe2 100644
--- a/Source/WebCore/accessibility/mac/AXObjectCacheMac.mm
+++ b/Source/WebCore/accessibility/mac/AXObjectCacheMac.mm
@@ -917,6 +917,30 @@ std::optional<SimpleRange> rangeForTextMarkerRange(AXObjectCache* cache, AXTextM
     return cache->rangeForUnorderedCharacterOffsets(startCharacterOffset, endCharacterOffset);
 }

+void AXObjectCache::onSelectedTextChanged(const VisiblePositionRange& selection)
+{
+#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
+    if (auto tree = AXIsolatedTree::treeForPageID(m_pageID)) {
+        if (selection.isNull())
+            tree->setSelectedTextMarkerRange(std::pair(TextMarkerData(), TextMarkerData()));
+        else {
+            auto startTextMarker = textMarkerDataForVisiblePosition(selection.start);
+            auto endTextMarker = textMarkerDataForVisiblePosition(selection.end);
+
+            if (startTextMarker && endTextMarker) {
+                auto* startObject = get(startTextMarker->node);
+                auto* endObject = get(endTextMarker->node);
+                createIsolatedObjectIfNeeded(*startObject, m_pageID);
+                createIsolatedObjectIfNeeded(*endObject, m_pageID);
+                tree->setSelectedTextMarkerRange(std::pair(startTextMarker.value(), endTextMarker.value()));
+            }
+        }
+    }
+#else
+    UNUSED_PARAM(selection);
+#endif
+}
+
 }

 #endif // ENABLE(ACCESSIBILITY) && PLATFORM(MAC)
Comment 7 Joshua Hoffman 2023-10-02 15:27:16 PDT
Comment on attachment 467992 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=467992&action=review

>>> COMMIT_MESSAGE:12
>>> +From there, a new method, onSelectedTextChanged, handles updating the isolated tree's
>> 
>> do we have a way to cache this data if accessibility was not enabled initially?
> 
> Good point—when we create an isolated tree we should just be able to grab the visible selection like we do in the live object? I can add that to this patch.

Investigated this and it turns out we already get a notifyAccessibilityForSelectionChange when AX is enabled (see LayoutTests/accessibility/mac/text-marker-p-tags.html which already verifies this), so this is handled.
Comment 8 Joshua Hoffman 2023-10-02 15:34:04 PDT
(In reply to Andres Gonzalez from comment #6)
> (In reply to Joshua Hoffman from comment #3)
> > Created attachment 467992 [details]
> > Patch
> 
> AG: is this also true when the selection changes happens in non-text objects
> like images and buttons?

Yes—any frame selection change will also update this. I'll make that note more clear in the commit message. 

> +#if PLATFORM(MAC)
> +    virtual AXTextMarkerRange selectedTextMarkerRange() = 0;
> +#endif
> 
> AG: don't we need this in other platforms? It should work the same.
> 

selectedTextMarkerRange is only called inside WebAccessibilityObjectWrapperMac & the Mac AccessibilityUIElement—so it will only be needed on that platform.
Comment 9 Joshua Hoffman 2023-10-02 20:03:36 PDT
Created attachment 468035 [details]
Patch
Comment 10 chris fleizach 2023-10-02 23:18:34 PDT
Comment on attachment 468035 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=468035&action=review

> Source/WebCore/accessibility/mac/AXObjectCacheMac.mm:930
> +            if (startPosition.isNull() || endPosition.isNull())

we may want to setSelectedTextMarkerRange({ }) in these failure cases too
Comment 11 Joshua Hoffman 2023-10-03 08:37:38 PDT
Created attachment 468047 [details]
Patch
Comment 12 Joshua Hoffman 2023-10-03 08:38:17 PDT
Comment on attachment 468035 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=468035&action=review

>> Source/WebCore/accessibility/mac/AXObjectCacheMac.mm:930
>> +            if (startPosition.isNull() || endPosition.isNull())
> 
> we may want to setSelectedTextMarkerRange({ }) in these failure cases too

Updated this code to do this in the latest patch.
Comment 13 EWS 2023-10-03 19:51:36 PDT
Committed 268821@main (a1746d78059d): <https://commits.webkit.org/268821@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 468047 [details].