Skip to content

Commit

Permalink
[a11y-during-render] Move dirty_object list into Blink
Browse files Browse the repository at this point in the history
Bug: 1068668, 1342801

Change-Id: Ie88eee26ba09431364758dd4509a476575befa93
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3911618
Commit-Queue: Chris Harrelson <chrishtr@chromium.org>
Reviewed-by: Stefan Zager <szager@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1051477}
  • Loading branch information
chrishtr authored and Chromium LUCI CQ committed Sep 26, 2022
1 parent 4a2c207 commit af5540a
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 155 deletions.
107 changes: 13 additions & 94 deletions content/renderer/accessibility/render_accessibility_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ void RenderAccessibilityImpl::Reset(int32_t reset_token) {
if (ax_context_)
ax_context_->ResetSerializer();
pending_events_.clear();
dirty_objects_.clear();
ax_context_->ClearDirtyObjects();

const WebDocument& document = GetMainDocument();
if (!document.IsNull()) {
Expand All @@ -514,11 +514,15 @@ void RenderAccessibilityImpl::MarkWebAXObjectDirty(
ax::mojom::Event event_type) {
DCHECK(obj.AccessibilityIsIncludedInTree())
<< "Cannot serialize unincluded object: " << obj.ToString(true).Utf8();
EnqueueDirtyObject(obj, event_from, event_from_action, event_intents,
dirty_objects_.end());

if (subtree)
obj.InvalidateSerializerSubtree();
obj.MarkDirty(subtree, event_from, event_from_action, event_intents);

NotifyWebAXObjectMarkedDirty(obj, event_type);
}

void RenderAccessibilityImpl::NotifyWebAXObjectMarkedDirty(
const blink::WebAXObject& obj,
ax::mojom::Event event_type) {

// If the event occurred on the focused object, process immediately.
// kLayoutComplete is an exception because it always fires on the root
Expand Down Expand Up @@ -648,23 +652,6 @@ bool RenderAccessibilityImpl::IsImmediateProcessingRequiredForEvent(
}
}

std::list<std::unique_ptr<AXDirtyObject>>::iterator
RenderAccessibilityImpl::EnqueueDirtyObject(
const blink::WebAXObject& obj,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action,
std::vector<ui::AXEventIntent> event_intents,
std::list<std::unique_ptr<AXDirtyObject>>::iterator insertion_point) {
DCHECK(!obj.IsDetached());
AXDirtyObject* dirty_object = new AXDirtyObject();
dirty_object->obj = obj;
dirty_object->event_from = event_from;
dirty_object->event_from_action = event_from_action;
dirty_object->event_intents = event_intents;
return std::next(dirty_objects_.insert(
insertion_point, base::WrapUnique<AXDirtyObject>(dirty_object)));
}

int RenderAccessibilityImpl::GetDeferredEventsDelay() {
// The amount of time, in milliseconds, to wait before sending non-interactive
// events that are deferred before the initial page load.
Expand Down Expand Up @@ -1052,87 +1039,23 @@ bool RenderAccessibilityImpl::SerializeUpdatesAndEvents(
// time to inject a stylesheet for image annotation debugging.
bool had_load_complete_messages = false;

// Dirty objects can be added as a result of serialization. For example,
// as children are iterated during depth first traversal in the serializer,
// the children sometimes need to be created. The initialization of these
// new children can lead to the discovery of parenting changes via
// aria-owns, or name changes on an ancestor that collects its name its from
// contents. In some cases this has led to an infinite loop, as the
// serialization of new dirty objects keeps adding new dirty objects to
// consider. The infinite loop is avoided by tracking the number of dirty
// objects that can be serialized from the loop, which is the initial
// number of dirty objects + kMaxExtraDirtyObjectsToSerialize.
// Allowing kMaxExtraDirtyObjectsToSerialize ensures that most important
// additional related changes occur at the same time, and that dump event
// tests have consistent results (the results change when dirty objects are
// processed in separate batches).
constexpr int kMaxExtraDirtyObjectsToSerialize = 100;
size_t num_remaining_objects_to_serialize =
dirty_objects_.size() + kMaxExtraDirtyObjectsToSerialize;

// Keep track of IDs serialized so we don't serialize the same node twice.
std::set<int32_t> already_serialized_ids;

// Serialize all dirty objects in the list at this point in time, stopping
// either when the queue is empty, or the number of remaining objects to
// serialize has been reached.
while (!dirty_objects_.empty() && --num_remaining_objects_to_serialize > 0) {
std::unique_ptr<AXDirtyObject> current_dirty_object =
std::move(dirty_objects_.front());
dirty_objects_.pop_front();
auto obj = current_dirty_object->obj;

// Cannot serialize detached or unincluded object.
// Dirty objects can be added using MarkWebAXObjectDirty(obj) from other
// parts of the code as well, so we need to ensure the object still exists
// and is still included in the tree. Only included objects are marked
// dirty, but this can happen if the object becomes unincluded after it was
// originally marked dirty, in which case a children changed will also be
// fired on the included ancestor. The children changed event on the
// ancestor means that attempting to serialize this unincluded object is not
// necessary.
if (!obj.AccessibilityIsIncludedInTree())
continue;

DCHECK(obj.AxID() != ui::kInvalidAXNodeID);

// Further down this loop, we update |already_serialized_ids| with all IDs
// actually serialized. However, add this object's ID first because there's
// a chance that we try to serialize this object but the serializer ends up
// skipping it. That's probably a Blink bug if that happens, but still we
// need to make sure we don't keep trying the same object over again.
if (!already_serialized_ids.insert(obj.AxID()).second)
continue; // No insertion, was already present.

ui::AXTreeUpdate update;
update.event_from = current_dirty_object->event_from;
update.event_from_action = current_dirty_object->event_from_action;
update.event_intents = current_dirty_object->event_intents;
// If there's a plugin, force the tree data to be generated in every
// message so the plugin can merge its own tree data changes.
if (plugin_tree_source_)
update.has_tree_data = true;

if (!obj.SerializeChanges(&update)) {
VLOG(1) << "Failed to serialize one accessibility event.";
continue;
}
ax_context_->SerializeDirtyObjects(updates, already_serialized_ids,
!!plugin_tree_source_);

for (auto& update : updates) {
if (update.node_id_to_clear > 0)
invalidate_plugin_subtree = true;

if (plugin_tree_source_)
AddPluginTreeToUpdate(&update, invalidate_plugin_subtree);

AddImageAnnotations(document, update.nodes);

DCHECK_GT(update.nodes.size(), 0U);
for (auto& node : update.nodes) {
DCHECK(node.id != ui::kInvalidAXNodeID);
already_serialized_ids.insert(node.id);
}

updates.push_back(update);
}

// Loop over each event and generate an updated event message.
Expand Down Expand Up @@ -1237,7 +1160,7 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() {
}
}

if (pending_events_.empty() && dirty_objects_.empty()) {
if (pending_events_.empty() && !ax_context_->HasDirtyObjects()) {
// By default, assume the next batch does not have interactive events, and
// defer so that the batch of events is larger. If any interactive events
// come in, the batch will be processed immediately.
Expand Down Expand Up @@ -1622,8 +1545,4 @@ void RenderAccessibilityImpl::ResetUKMData() {
last_ukm_source_id_ = ukm::kInvalidSourceId;
}

AXDirtyObject::AXDirtyObject() = default;
AXDirtyObject::AXDirtyObject(const AXDirtyObject& other) = default;
AXDirtyObject::~AXDirtyObject() = default;

} // namespace content
27 changes: 3 additions & 24 deletions content/renderer/accessibility/render_accessibility_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,6 @@ class RenderAccessibilityManager;

using BlinkAXTreeSerializer = ui::AXTreeSerializer<blink::WebAXObject>;

struct AXDirtyObject {
AXDirtyObject();
AXDirtyObject(const AXDirtyObject& other);
~AXDirtyObject();
blink::WebAXObject obj;
ax::mojom::EventFrom event_from;
ax::mojom::Action event_from_action;
std::vector<ui::AXEventIntent> event_intents;
};

// The browser process implements native accessibility APIs, allowing assistive
// technology (e.g., screen readers, magnifiers) to access and control the web
// contents with high-level APIs. These APIs are also used by automation tools,
Expand Down Expand Up @@ -129,6 +119,9 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility,
std::vector<ui::AXEventIntent> event_intents = {},
ax::mojom::Event event_type = ax::mojom::Event::kNone);

void NotifyWebAXObjectMarkedDirty(const blink::WebAXObject& obj,
ax::mojom::Event event_type = ax::mojom::Event::kNone);

// Returns the main top-level document for this page, or NULL if there's
// no view or frame.
blink::WebDocument GetMainDocument();
Expand Down Expand Up @@ -175,15 +168,6 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility,
kNotWaiting
};

// Add an AXDirtyObject to the dirty_objects_ queue.
// Returns an iterator pointing just after the newly inserted object.
std::list<std::unique_ptr<AXDirtyObject>>::iterator EnqueueDirtyObject(
const blink::WebAXObject& obj,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action,
std::vector<ui::AXEventIntent> event_intents,
std::list<std::unique_ptr<AXDirtyObject>>::iterator insertion_point);

// Callback that will be called from the browser upon handling the message
// previously sent to it via SendPendingAccessibilityEvents().
void OnAccessibilityEventsHandled();
Expand Down Expand Up @@ -279,11 +263,6 @@ class CONTENT_EXPORT RenderAccessibilityImpl : public RenderAccessibility,
// sent to the browser.
std::vector<ui::AXEvent> pending_events_;

// Objects that need to be re-serialized, the next time
// we send an event bundle to the browser - but don't specifically need
// an event fired.
std::list<std::unique_ptr<AXDirtyObject>> dirty_objects_;

using PluginAXTreeSerializer = ui::AXTreeSerializer<const ui::AXNode*>;
std::unique_ptr<PluginAXTreeSerializer> plugin_serializer_;
PluginAXTreeSource* plugin_tree_source_;
Expand Down
9 changes: 3 additions & 6 deletions content/renderer/render_frame_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4442,16 +4442,13 @@ void RenderFrameImpl::PostAccessibilityEvent(const ui::AXEvent& event) {
event);
}

void RenderFrameImpl::MarkWebAXObjectDirty(
const blink::WebAXObject& obj,
bool subtree,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action) {
void RenderFrameImpl::NotifyWebAXObjectMarkedDirty(
const blink::WebAXObject& obj) {
if (!IsAccessibilityEnabled())
return;

render_accessibility_manager_->GetRenderAccessibilityImpl()
->MarkWebAXObjectDirty(obj, subtree, event_from, event_from_action);
->NotifyWebAXObjectMarkedDirty(obj);
}

void RenderFrameImpl::AddObserver(RenderFrameObserver* observer) {
Expand Down
5 changes: 1 addition & 4 deletions content/renderer/render_frame_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -604,10 +604,7 @@ class CONTENT_EXPORT RenderFrameImpl
bool AllowContentInitiatedDataUrlNavigations(
const blink::WebURL& url) override;
void PostAccessibilityEvent(const ui::AXEvent& event) override;
void MarkWebAXObjectDirty(const blink::WebAXObject& obj,
bool subtree,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action) override;
void NotifyWebAXObjectMarkedDirty(const blink::WebAXObject& object) override;
void CheckIfAudioSinkExistsAndIsAuthorized(
const blink::WebString& sink_id,
blink::WebSetSinkIdCompleteCallback callback) override;
Expand Down
10 changes: 3 additions & 7 deletions content/web_test/renderer/web_frame_test_proxy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -676,11 +676,8 @@ void WebFrameTestProxy::PostAccessibilityEvent(const ui::AXEvent& event) {
RenderFrameImpl::PostAccessibilityEvent(event);
}

void WebFrameTestProxy::MarkWebAXObjectDirty(
const blink::WebAXObject& object,
bool subtree,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action) {
void WebFrameTestProxy::NotifyWebAXObjectMarkedDirty(
const blink::WebAXObject& object) {
HandleWebAccessibilityEvent(object, "MarkDirty",
std::vector<ui::AXEventIntent>());

Expand All @@ -690,8 +687,7 @@ void WebFrameTestProxy::MarkWebAXObjectDirty(
if (object.IsDetached())
return; // |this| is invalid.

RenderFrameImpl::MarkWebAXObjectDirty(object, subtree, event_from,
event_from_action);
RenderFrameImpl::NotifyWebAXObjectMarkedDirty(object);
}

void WebFrameTestProxy::HandleWebAccessibilityEvent(
Expand Down
5 changes: 1 addition & 4 deletions content/web_test/renderer/web_frame_test_proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,7 @@ class WebFrameTestProxy : public RenderFrameImpl,
ForRedirect for_redirect) override;
void BeginNavigation(std::unique_ptr<blink::WebNavigationInfo> info) override;
void PostAccessibilityEvent(const ui::AXEvent& event) override;
void MarkWebAXObjectDirty(const blink::WebAXObject& object,
bool subtree,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action) override;
void NotifyWebAXObjectMarkedDirty(const blink::WebAXObject& object) override;
void CheckIfAudioSinkExistsAndIsAuthorized(
const blink::WebString& sink_id,
blink::WebSetSinkIdCompleteCallback completion_callback) override;
Expand Down
1 change: 1 addition & 0 deletions third_party/blink/public/web/DEPS
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ include_rules = [
"+third_party/blink/public/web",
"+ui/accessibility/ax_enums.mojom-shared.h",
"+ui/accessibility/ax_event.h",
"+ui/accessibility/ax_event_intent.h",
"+ui/accessibility/ax_mode.h",
"+ui/accessibility/ax_tree_id.h",
"+ui/base/ime/mojom/ime_types.mojom-shared.h",
Expand Down
14 changes: 14 additions & 0 deletions third_party/blink/public/web/web_ax_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ class BLINK_EXPORT WebAXContext {
void MarkAllImageAXObjectsDirty(
ax::mojom::Action event_from_action);

// Serialize all AXObjects that are dirty (have changed their state since
// the last serialization) into |updates|. (Heuristically) skips
// serializing dirty nodes whose AX id is in |already_serialized_ids|, and
// adds serialized dirty objects into |already_serialized_ids|.
void SerializeDirtyObjects(std::vector<ui::AXTreeUpdate>& updates,
std::set<int32_t>& already_serialized_ids,
bool has_plugin_tree_source);

// Clears out the list of dirty AXObjects.
void ClearDirtyObjects();

// Returns true if any AXObject is dirty.
bool HasDirtyObjects();

private:
std::unique_ptr<AXContext> private_;
};
Expand Down
11 changes: 7 additions & 4 deletions third_party/blink/public/web/web_ax_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "third_party/blink/public/platform/web_vector.h"
#include "third_party/blink/public/web/web_ax_enums.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/ax_mode.h"

namespace gfx {
Expand Down Expand Up @@ -332,10 +333,12 @@ class BLINK_EXPORT WebAXObject {
gfx::Transform& container_transform,
bool* clips_children = nullptr) const;

// Retrieves a vector of all WebAXObjects in this document whose
// bounding boxes may have changed since the last query. Sends that vector
// via mojo to the browser process.
void SerializeLocationChanges() const;
// Marks ths object as dirty (needing serialization). If subtree is true,
// the entire AX subtree should be invalidated as well.
void MarkDirty(bool subtree,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action,
std::vector<ui::AXEventIntent> event_intents) const;

// Exchanges a WebAXObject with another.
void Swap(WebAXObject& other);
Expand Down
13 changes: 3 additions & 10 deletions third_party/blink/public/web/web_local_frame_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -623,16 +623,9 @@ class BLINK_EXPORT WebLocalFrameClient {
// Notifies the embedder about an accessibility event on a WebAXObject.
virtual void PostAccessibilityEvent(const ui::AXEvent& event) {}

// Notifies the embedder that a WebAXObject is dirty and its state needs
// to be serialized again. If |subtree| is true, the entire subtree is
// dirty.
// |event_from| and |event_from_action| annotate this node change with info
// about the event which caused the change. For example, an event from a user
// or an event from a focus action.
virtual void MarkWebAXObjectDirty(const WebAXObject&,
bool subtree,
ax::mojom::EventFrom event_from,
ax::mojom::Action event_from_action) {}
// Notifies tests that a WebAXObject is dirty and its state needs
// to be serialized again.
virtual void NotifyWebAXObjectMarkedDirty(const WebAXObject&) {}

// Audio Output Devices API --------------------------------------------

Expand Down
21 changes: 21 additions & 0 deletions third_party/blink/renderer/core/accessibility/ax_object_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,27 @@ class CORE_EXPORT AXObjectCache : public GarbageCollected<AXObjectCache> {
virtual void MarkAllImageAXObjectsDirty(
ax::mojom::blink::Action event_from_action) = 0;

// Notifies that an AXObject is dirty and its state needs
// to be serialized again. If |subtree| is true, the entire subtree is
// dirty.
// |event_from| and |event_from_action| annotate this node change with info
// about the event which caused the change. For example, an event from a user
// or an event from a focus action.
virtual void MarkAXObjectDirty(
AXObject* obj,
bool subtree,
ax::mojom::blink::EventFrom event_from,
ax::mojom::blink::Action event_from_action,
const std::vector<ui::AXEventIntent>& event_intents) = 0;

virtual void SerializeDirtyObjects(std::vector<ui::AXTreeUpdate>& updates,
std::set<int32_t>& already_serialized_ids,
bool has_plugin_tree_source) = 0;

virtual void ClearDirtyObjects() = 0;

virtual bool HasDirtyObjects() = 0;

// Ensure that a call to ProcessDeferredAccessibilityEvents() will occur soon.
virtual void ScheduleVisualUpdate(Document& document) = 0;

Expand Down
Loading

0 comments on commit af5540a

Please sign in to comment.