diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/base/NotificationController.cpp | 1107 |
1 files changed, 1107 insertions, 0 deletions
diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp new file mode 100644 index 0000000000..63786861f1 --- /dev/null +++ b/accessible/base/NotificationController.cpp @@ -0,0 +1,1107 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "NotificationController.h" + +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "LocalAccessible-inl.h" +#include "nsEventShell.h" +#include "TextLeafAccessible.h" +#include "TextUpdater.h" + +#include "nsIContentInlines.h" + +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ipc/ProcessChild.h" +#include "mozilla/PresShell.h" +#include "mozilla/ProfilerMarkers.h" +#include "nsAccessibilityService.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector +//////////////////////////////////////////////////////////////////////////////// + +NotificationController::NotificationController(DocAccessible* aDocument, + PresShell* aPresShell) + : EventQueue(aDocument), + mObservingState(eNotObservingRefresh), + mPresShell(aPresShell), + mEventGeneration(0) { + // Schedule initial accessible tree construction. + ScheduleProcessing(); +} + +NotificationController::~NotificationController() { + NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!"); + if (mDocument) { + Shutdown(); + } + MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, + "Must unregister before being destroyed"); +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: AddRef/Release and cycle collection + +NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController) +NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController) + +NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController) + if (tmp->mDocument) { + tmp->Shutdown(); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments) + for (const auto& entry : tmp->mContentInsertions) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions key"); + cb.NoteXPCOMChild(entry.GetKey()); + nsTArray<nsCOMPtr<nsIContent>>* list = entry.GetData().get(); + for (uint32_t i = 0; i < list->Length(); i++) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions value item"); + cb.NoteXPCOMChild(list->ElementAt(i)); + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFocusEvent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelocations) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: public + +void NotificationController::Shutdown() { + if (mObservingState != eNotObservingRefresh && + mPresShell->RemoveRefreshObserver(this, FlushType::Display)) { + // Note, this was our last chance to unregister, since we're about to + // clear mPresShell further down in this function. + mObservingState = eNotObservingRefresh; + } + MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, + "Must unregister before being destroyed (and we just " + "passed our last change to unregister)"); + // Immediately null out mPresShell, to prevent us from being registered as a + // refresh observer again. + mPresShell = nullptr; + + // Shutdown handling child documents. + int32_t childDocCount = mHangingChildDocuments.Length(); + for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { + if (!mHangingChildDocuments[idx]->IsDefunct()) { + mHangingChildDocuments[idx]->Shutdown(); + } + } + + mHangingChildDocuments.Clear(); + + mDocument = nullptr; + + mTextArray.Clear(); + mContentInsertions.Clear(); + mNotifications.Clear(); + mFocusEvent = nullptr; + mEvents.Clear(); + mRelocations.Clear(); +} + +void NotificationController::CoalesceHideEvent(AccHideEvent* aHideEvent) { + LocalAccessible* parent = aHideEvent->LocalParent(); + while (parent) { + if (parent->IsDoc()) { + break; + } + + if (parent->HideEventTarget()) { + DropMutationEvent(aHideEvent); + break; + } + + if (parent->ShowEventTarget()) { + AccShowEvent* showEvent = + downcast_accEvent(mMutationMap.GetEvent(parent, EventMap::ShowEvent)); + if (showEvent->EventGeneration() < aHideEvent->EventGeneration()) { + DropMutationEvent(aHideEvent); + break; + } + } + + parent = parent->LocalParent(); + } +} + +bool NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) { + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + // We have to allow there to be a hide and then a show event for a target + // because of targets getting moved. However we need to coalesce a show and + // then a hide for a target which means we need to check for that here. + if (aEvent->GetAccessible()->ShowEventTarget()) { + AccTreeMutationEvent* showEvent = + mMutationMap.GetEvent(aEvent->GetAccessible(), EventMap::ShowEvent); + DropMutationEvent(showEvent); + return false; + } + + // Don't queue a hide event on an accessible that's already being moved. It + // or an ancestor should already have a hide event queued. + if (mDocument && + mDocument->IsAccessibleBeingMoved(aEvent->GetAccessible())) { + return false; + } + + // If this is an additional hide event, the accessible may be hidden, or + // moved again after a move. Preserve the original hide event since + // its properties are consistent with the tree that existed before + // the next batch of mutation events is processed. + if (aEvent->GetAccessible()->HideEventTarget()) { + return false; + } + } + + AccMutationEvent* mutEvent = downcast_accEvent(aEvent); + mEventGeneration++; + mutEvent->SetEventGeneration(mEventGeneration); + + if (!mFirstMutationEvent) { + mFirstMutationEvent = aEvent; + ScheduleProcessing(); + } + + if (mLastMutationEvent) { + NS_ASSERTION(!mLastMutationEvent->NextEvent(), + "why isn't the last event the end?"); + mLastMutationEvent->SetNextEvent(aEvent); + } + + aEvent->SetPrevEvent(mLastMutationEvent); + mLastMutationEvent = aEvent; + mMutationMap.PutEvent(aEvent); + + // Because we could be hiding the target of a show event we need to get rid + // of any such events. + if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { + CoalesceHideEvent(downcast_accEvent(aEvent)); + + // mLastMutationEvent will point to something other than aEvent if and only + // if aEvent was just coalesced away. In that case a parent accessible + // must already have the required reorder and text change events so we are + // done here. + if (mLastMutationEvent != aEvent) { + return false; + } + } + + // We need to fire a reorder event after all of the events targeted at shown + // or hidden children of a container. So either queue a new one, or move an + // existing one to the end of the queue if the container already has a + // reorder event. + LocalAccessible* container = aEvent->GetAccessible()->LocalParent(); + RefPtr<AccReorderEvent> reorder; + if (!container->ReorderEventTarget()) { + reorder = new AccReorderEvent(container); + container->SetReorderEventTarget(true); + mMutationMap.PutEvent(reorder); + + // Since this is the first child of container that is changing, the name + // and/or description of dependent Accessibles may be changing. + if (PushNameOrDescriptionChange(aEvent)) { + ScheduleProcessing(); + } + } else { + AccReorderEvent* event = downcast_accEvent( + mMutationMap.GetEvent(container, EventMap::ReorderEvent)); + reorder = event; + if (mFirstMutationEvent == event) { + mFirstMutationEvent = event->NextEvent(); + } else { + event->PrevEvent()->SetNextEvent(event->NextEvent()); + } + + event->NextEvent()->SetPrevEvent(event->PrevEvent()); + event->SetNextEvent(nullptr); + } + + reorder->SetEventGeneration(mEventGeneration); + reorder->SetPrevEvent(mLastMutationEvent); + mLastMutationEvent->SetNextEvent(reorder); + mLastMutationEvent = reorder; + + // It is not possible to have a text change event for something other than a + // hyper text accessible. + if (!container->IsHyperText()) { + return true; + } + + MOZ_ASSERT(mutEvent); + + nsString text; + aEvent->GetAccessible()->AppendTextTo(text); + if (text.IsEmpty()) { + return true; + } + + LocalAccessible* target = aEvent->GetAccessible(); + int32_t offset = container->AsHyperText()->GetChildOffset(target); + AccTreeMutationEvent* prevEvent = aEvent->PrevEvent(); + while (prevEvent && + prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + prevEvent = prevEvent->PrevEvent(); + } + + if (prevEvent && + prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE && + mutEvent->IsHide()) { + AccHideEvent* prevHide = downcast_accEvent(prevEvent); + AccTextChangeEvent* prevTextChange = prevHide->mTextChangeEvent; + if (prevTextChange && prevHide->LocalParent() == mutEvent->LocalParent()) { + if (prevHide->mNextSibling == target) { + target->AppendTextTo(prevTextChange->mModifiedText); + prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } else if (prevHide->mPrevSibling == target) { + nsString temp; + target->AppendTextTo(temp); + + uint32_t extraLen = temp.Length(); + temp += prevTextChange->mModifiedText; + ; + prevTextChange->mModifiedText = temp; + prevTextChange->mStart -= extraLen; + prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } + } + } else if (prevEvent && mutEvent->IsShow() && + prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { + AccShowEvent* prevShow = downcast_accEvent(prevEvent); + AccTextChangeEvent* prevTextChange = prevShow->mTextChangeEvent; + if (prevTextChange && prevShow->LocalParent() == target->LocalParent()) { + int32_t index = target->IndexInParent(); + int32_t prevIndex = prevShow->GetAccessible()->IndexInParent(); + if (prevIndex + 1 == index) { + target->AppendTextTo(prevTextChange->mModifiedText); + prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } else if (index + 1 == prevIndex) { + nsString temp; + target->AppendTextTo(temp); + prevTextChange->mStart -= temp.Length(); + temp += prevTextChange->mModifiedText; + prevTextChange->mModifiedText = temp; + prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); + } + } + } + + if (!mutEvent->mTextChangeEvent) { + mutEvent->mTextChangeEvent = new AccTextChangeEvent( + container, offset, text, mutEvent->IsShow(), + aEvent->mIsFromUserInput ? eFromUserInput : eNoUserInput); + } + + return true; +} + +void NotificationController::DropMutationEvent(AccTreeMutationEvent* aEvent) { + const uint32_t eventType = aEvent->GetEventType(); + MOZ_ASSERT(eventType != nsIAccessibleEvent::EVENT_INNER_REORDER, + "Inner reorder has already been dropped, cannot drop again"); + if (eventType == nsIAccessibleEvent::EVENT_REORDER) { + // We don't fully drop reorder events, we just change them to inner reorder + // events. + AccReorderEvent* reorderEvent = downcast_accEvent(aEvent); + + MOZ_ASSERT(reorderEvent); + reorderEvent->SetInner(); + return; + } + if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + // unset the event bits since the event isn't being fired any more. + aEvent->GetAccessible()->SetShowEventTarget(false); + } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) { + // unset the event bits since the event isn't being fired any more. + aEvent->GetAccessible()->SetHideEventTarget(false); + + AccHideEvent* hideEvent = downcast_accEvent(aEvent); + MOZ_ASSERT(hideEvent); + + if (hideEvent->NeedsShutdown()) { + mDocument->ShutdownChildrenInSubtree(aEvent->GetAccessible()); + } + } else { + MOZ_ASSERT_UNREACHABLE("Mutation event has non-mutation event type"); + } + + // Do the work to splice the event out of the list. + if (mFirstMutationEvent == aEvent) { + mFirstMutationEvent = aEvent->NextEvent(); + } else { + aEvent->PrevEvent()->SetNextEvent(aEvent->NextEvent()); + } + + if (mLastMutationEvent == aEvent) { + mLastMutationEvent = aEvent->PrevEvent(); + } else { + aEvent->NextEvent()->SetPrevEvent(aEvent->PrevEvent()); + } + + aEvent->SetPrevEvent(nullptr); + aEvent->SetNextEvent(nullptr); + mMutationMap.RemoveEvent(aEvent); +} + +void NotificationController::CoalesceMutationEvents() { + AccTreeMutationEvent* event = mFirstMutationEvent; + while (event) { + AccTreeMutationEvent* nextEvent = event->NextEvent(); + uint32_t eventType = event->GetEventType(); + if (event->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { + LocalAccessible* acc = event->GetAccessible(); + while (acc) { + if (acc->IsDoc()) { + break; + } + + // if a parent of the reorder event's target is being hidden that + // hide event's target must have a parent that is also a reorder event + // target. That means we don't need this reorder event. + if (acc->HideEventTarget()) { + DropMutationEvent(event); + break; + } + + LocalAccessible* parent = acc->LocalParent(); + if (parent && parent->ReorderEventTarget()) { + AccReorderEvent* reorder = downcast_accEvent( + mMutationMap.GetEvent(parent, EventMap::ReorderEvent)); + + // We want to make sure that a reorder event comes after any show or + // hide events targeted at the children of its target. We keep the + // invariant that event generation goes up as you are farther in the + // queue, so we want to use the spot of the event with the higher + // generation number, and keep that generation number. + if (reorder && + reorder->EventGeneration() < event->EventGeneration()) { + reorder->SetEventGeneration(event->EventGeneration()); + + // It may be true that reorder was before event, and we coalesced + // away all the show / hide events between them. In that case + // event is already immediately after reorder in the queue and we + // do not need to rearrange the list of events. + if (event != reorder->NextEvent()) { + // There really should be a show or hide event before the first + // reorder event. + if (reorder->PrevEvent()) { + reorder->PrevEvent()->SetNextEvent(reorder->NextEvent()); + } else { + mFirstMutationEvent = reorder->NextEvent(); + } + + reorder->NextEvent()->SetPrevEvent(reorder->PrevEvent()); + event->PrevEvent()->SetNextEvent(reorder); + reorder->SetPrevEvent(event->PrevEvent()); + event->SetPrevEvent(reorder); + reorder->SetNextEvent(event); + } + } + DropMutationEvent(event); + break; + } + + acc = parent; + } + } else if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + LocalAccessible* parent = event->GetAccessible()->LocalParent(); + while (parent) { + if (parent->IsDoc()) { + break; + } + + // if the parent of a show event is being either shown or hidden then + // we don't need to fire a show event for a subtree of that change. + if (parent->ShowEventTarget() || parent->HideEventTarget()) { + DropMutationEvent(event); + break; + } + + parent = parent->LocalParent(); + } + } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) { + MOZ_ASSERT(eventType == nsIAccessibleEvent::EVENT_HIDE, + "mutation event list has an invalid event"); + + AccHideEvent* hideEvent = downcast_accEvent(event); + CoalesceHideEvent(hideEvent); + } + + event = nextEvent; + } +} + +void NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) { + // Schedule child document binding to the tree. + mHangingChildDocuments.AppendElement(aDocument); + ScheduleProcessing(); +} + +void NotificationController::ScheduleContentInsertion( + LocalAccessible* aContainer, nsTArray<nsCOMPtr<nsIContent>>& aInsertions) { + if (!aInsertions.IsEmpty()) { + mContentInsertions.GetOrInsertNew(aContainer)->AppendElements(aInsertions); + ScheduleProcessing(); + } +} + +void NotificationController::ScheduleProcessing() { + // If notification flush isn't planned yet, start notification flush + // asynchronously (after style and layout). + // Note: the mPresShell null-check might be unnecessary; it's just to prevent + // a null-deref here, if we somehow get called after we've been shut down. + if (mObservingState == eNotObservingRefresh && mPresShell) { + if (mPresShell->AddRefreshObserver(this, FlushType::Display, + "Accessibility notifications")) { + mObservingState = eRefreshObserving; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: protected + +bool NotificationController::IsUpdatePending() { + return mPresShell->IsLayoutFlushObserver() || + mObservingState == eRefreshProcessingForUpdate || WaitingForParent() || + mContentInsertions.Count() != 0 || mNotifications.Length() != 0 || + !mTextArray.IsEmpty() || + !mDocument->HasLoadState(DocAccessible::eTreeConstructed); +} + +bool NotificationController::WaitingForParent() { + DocAccessible* parentdoc = mDocument->ParentDocument(); + if (!parentdoc) { + return false; + } + + NotificationController* parent = parentdoc->mNotificationController; + if (!parent || parent == this) { + // Do not wait for nothing or ourselves + return false; + } + + // Wait for parent's notifications processing + return parent->mContentInsertions.Count() != 0 || + parent->mNotifications.Length() != 0; +} + +void NotificationController::ProcessMutationEvents() { + // Firing an event can indirectly run script; e.g. an XPCOM event observer + // or querying a XUL interface. Further mutations might be queued as a result. + // It's important that the mutation queue and state bits from one tick don't + // interfere with the next tick. Otherwise, we can end up dropping events. + // Therefore: + // 1. Clear the state bits, which we only need for coalescence. + for (AccTreeMutationEvent* event = mFirstMutationEvent; event; + event = event->NextEvent()) { + LocalAccessible* acc = event->GetAccessible(); + acc->SetShowEventTarget(false); + acc->SetHideEventTarget(false); + acc->SetReorderEventTarget(false); + } + // 2. Keep the current queue locally, but clear the queue on the instance. + RefPtr<AccTreeMutationEvent> firstEvent = mFirstMutationEvent; + mFirstMutationEvent = mLastMutationEvent = nullptr; + mMutationMap.Clear(); + mEventGeneration = 0; + + // Group the show events by the parent of their target. + nsTHashMap<nsPtrHashKey<LocalAccessible>, nsTArray<AccTreeMutationEvent*>> + showEvents; + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_SHOW) { + continue; + } + + LocalAccessible* parent = event->GetAccessible()->LocalParent(); + showEvents.LookupOrInsert(parent).AppendElement(event); + } + + // We need to fire show events for the children of an accessible in the order + // of their indices at this point. So sort each set of events for the same + // container by the index of their target. We do this before firing any events + // because firing an event might indirectly run script which might alter the + // tree, breaking our sort. However, we don't actually fire the events yet. + for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { + struct AccIdxComparator { + bool LessThan(const AccTreeMutationEvent* a, + const AccTreeMutationEvent* b) const { + int32_t aIdx = a->GetAccessible()->IndexInParent(); + int32_t bIdx = b->GetAccessible()->IndexInParent(); + MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && (a == b || aIdx != bIdx)); + return aIdx < bIdx; + } + bool Equals(const AccTreeMutationEvent* a, + const AccTreeMutationEvent* b) const { + DebugOnly<int32_t> aIdx = a->GetAccessible()->IndexInParent(); + DebugOnly<int32_t> bIdx = b->GetAccessible()->IndexInParent(); + MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && (a == b || aIdx != bIdx)); + return a == b; + } + }; + + nsTArray<AccTreeMutationEvent*>& events = iter.Data(); + events.Sort(AccIdxComparator()); + } + + // there is no reason to fire a hide event for a child of a show event + // target. That can happen if something is inserted into the tree and + // removed before the next refresh driver tick, but it should not be + // observable outside gecko so it should be safe to coalesce away any such + // events. This means that it should be fine to fire all of the hide events + // first, and then deal with any shown subtrees. + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != nsIAccessibleEvent::EVENT_HIDE) { + continue; + } + + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + AccMutationEvent* mutEvent = downcast_accEvent(event); + if (mutEvent->mTextChangeEvent) { + nsEventShell::FireEvent(mutEvent->mTextChangeEvent); + if (!mDocument) { + return; + } + } + + // Fire menupopup end event before a hide event if a menu goes away. + + // XXX: We don't look into children of hidden subtree to find hiding + // menupopup (as we did prior bug 570275) because we don't do that when + // menu is showing (and that's impossible until bug 606924 is fixed). + // Nevertheless we should do this at least because layout coalesces + // the changes before our processing and we may miss some menupopup + // events. Now we just want to be consistent in content insertion/removal + // handling. + if (event->mAccessible->ARIARole() == roles::MENUPOPUP) { + nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, + event->mAccessible); + if (!mDocument) { + return; + } + } + + AccHideEvent* hideEvent = downcast_accEvent(event); + if (hideEvent->NeedsShutdown()) { + mDocument->ShutdownChildrenInSubtree(event->mAccessible); + } + } + + // Fire the show events we sorted earlier. + for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { + nsTArray<AccTreeMutationEvent*>& events = iter.Data(); + for (AccTreeMutationEvent* event : events) { + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + AccMutationEvent* mutEvent = downcast_accEvent(event); + if (mutEvent->mTextChangeEvent) { + nsEventShell::FireEvent(mutEvent->mTextChangeEvent); + if (!mDocument) { + return; + } + } + } + } + + // Now we can fire the reorder events after all the show and hide events. + for (const uint32_t reorderType : {nsIAccessibleEvent::EVENT_INNER_REORDER, + nsIAccessibleEvent::EVENT_REORDER}) { + for (AccTreeMutationEvent* event = firstEvent; event; + event = event->NextEvent()) { + if (event->GetEventType() != reorderType) { + continue; + } + + if (event->GetAccessible()->IsDefunct()) { + // An inner reorder target may have been hidden itself and no + // longer bound to the document. + MOZ_ASSERT(reorderType == nsIAccessibleEvent::EVENT_INNER_REORDER, + "An 'outer' reorder target should not be defunct"); + continue; + } + + nsEventShell::FireEvent(event); + if (!mDocument) { + return; + } + + LocalAccessible* target = event->GetAccessible(); + target->Document()->MaybeNotifyOfValueChange(target); + if (!mDocument) { + return; + } + } + } + + // Our events are in a doubly linked list. Clear the pointers to reduce + // pressure on the cycle collector. Even though clearing the previous pointers + // removes cycles, this isn't enough. The cycle collector still gets bogged + // down when there are lots of mutation events if the next pointers aren't + // cleared. Even without the cycle collector, not clearing the next pointers + // potentially results in deep recursion because releasing each event releases + // its next event. + RefPtr<AccTreeMutationEvent> event = firstEvent; + while (event) { + RefPtr<AccTreeMutationEvent> next = event->NextEvent(); + event->SetNextEvent(nullptr); + event->SetPrevEvent(nullptr); + event = next; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// NotificationCollector: private + +void NotificationController::WillRefresh(mozilla::TimeStamp aTime) { + AUTO_PROFILER_MARKER_TEXT("NotificationController::WillRefresh", A11Y, {}, + ""_ns); + Telemetry::AutoTimer<Telemetry::A11Y_TREE_UPDATE_TIMING_MS> timer; + // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. + + AUTO_PROFILER_LABEL("NotificationController::WillRefresh", A11Y); + + // If mDocument is null, the document accessible that this notification + // controller was created for is now shut down. This means we've lost our + // ability to unregister ourselves, which is bad. (However, it also shouldn't + // be logically possible for us to get here with a null mDocument; the only + // thing that clears that pointer is our Shutdown() method, which first + // unregisters and fatally asserts if that fails). + MOZ_RELEASE_ASSERT( + mDocument, + "The document was shut down while refresh observer is attached!"); + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + // Wait until an update, we have started, or an interruptible reflow is + // finished. We also check the existance of our pres context and root pres + // context, since if we can't reach either of these the frame tree is being + // destroyed. + nsPresContext* pc = mPresShell->GetPresContext(); + if (mObservingState == eRefreshProcessing || + mObservingState == eRefreshProcessingForUpdate || + mPresShell->IsReflowInterrupted() || !pc || !pc->GetRootPresContext()) { + return; + } + + // Process parent's notifications before ours, to get proper ordering between + // e.g. tab event and content event. + if (WaitingForParent()) { + mDocument->ParentDocument()->mNotificationController->WillRefresh(aTime); + if (!mDocument || ipc::ProcessChild::ExpectingShutdown()) { + return; + } + } + + // Any generic notifications should be queued if we're processing content + // insertions or generic notifications. + mObservingState = eRefreshProcessingForUpdate; + + // Initial accessible tree construction. + if (!mDocument->HasLoadState(DocAccessible::eTreeConstructed)) { + // (1) If document is not bound to parent at this point, or + // (2) the PresShell is not initialized (and it isn't about:blank), + // then the document is not ready yet (process notifications later). + if (!mDocument->IsBoundToParent() || + (!mPresShell->DidInitialize() && + !mDocument->DocumentNode()->IsInitialDocument())) { + mObservingState = eRefreshObserving; + return; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree)) { + logging::MsgBegin("TREE", "initial tree created"); + logging::Address("document", mDocument); + logging::MsgEnd(); + } +#endif + + mDocument->DoInitialUpdate(); + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + NS_ASSERTION(mContentInsertions.Count() == 0, + "Pending content insertions while initial accessible tree " + "isn't created!"); + } + + mDocument->ProcessPendingUpdates(); + + // Process rendered text change notifications. Even though we want to process + // them in the order in which they were queued, we still want to avoid + // duplicates. + nsTHashSet<nsIContent*> textHash; + for (nsIContent* textNode : mTextArray) { + if (!textHash.EnsureInserted(textNode)) { + continue; // Already processed. + } + LocalAccessible* textAcc = mDocument->GetAccessible(textNode); + + // If the text node is not in tree or doesn't have a frame, or placed in + // another document, then this case should have been handled already by + // content removal notifications. + nsINode* containerNode = textNode->GetFlattenedTreeParentNode(); + if (!containerNode || textNode->OwnerDoc() != mDocument->DocumentNode()) { + MOZ_ASSERT(!textAcc, + "Text node was removed but accessible is kept alive!"); + continue; + } + + nsIFrame* textFrame = textNode->GetPrimaryFrame(); + if (!textFrame) { + MOZ_ASSERT(!textAcc, + "Text node isn't rendered but accessible is kept alive!"); + continue; + } + +#ifdef A11Y_LOG + nsIContent* containerElm = + containerNode->IsElement() ? containerNode->AsElement() : nullptr; +#endif + + nsIFrame::RenderedText text = textFrame->GetRenderedText( + 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, + nsIFrame::TrailingWhitespace::DontTrim); + + // Remove text accessible if rendered text is empty. + if (textAcc) { + if (text.mString.IsEmpty()) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree | logging::eText)) { + logging::MsgBegin("TREE", "text node lost its content; doc: %p", + mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEnd(); + } +#endif + + mDocument->ContentRemoved(textAcc); + continue; + } + + // Update text of the accessible and fire text change events. +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eText)) { + logging::MsgBegin("TEXT", "text may be changed; doc: %p", mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEntry( + "old text '%s'", + NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get()); + logging::MsgEntry("new text: '%s'", + NS_ConvertUTF16toUTF8(text.mString).get()); + logging::MsgEnd(); + } +#endif + + TextUpdater::Run(mDocument, textAcc->AsTextLeaf(), text.mString); + continue; + } + + // Append an accessible if rendered text is not empty. + if (!text.mString.IsEmpty()) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eTree | logging::eText)) { + logging::MsgBegin("TREE", "text node gains new content; doc: %p", + mDocument); + logging::Node("container", containerElm); + logging::Node("content", textNode); + logging::MsgEnd(); + } +#endif + + MOZ_ASSERT(mDocument->AccessibleOrTrueContainer(containerNode), + "Text node having rendered text hasn't accessible document!"); + + LocalAccessible* container = + mDocument->AccessibleOrTrueContainer(containerNode, true); + if (container) { + nsTArray<nsCOMPtr<nsIContent>>* list = + mContentInsertions.GetOrInsertNew(container); + list->AppendElement(textNode); + } + } + } + textHash.Clear(); + mTextArray.Clear(); + + // Process content inserted notifications to update the tree. + // Processing an insertion can indirectly run script (e.g. querying a XUL + // interface), which might result in another insertion being queued. + // We don't want to lose any queued insertions if this happens. Therefore, we + // move the current insertions into a temporary data structure and process + // them from there. Any insertions queued during processing will get handled + // in subsequent refresh driver ticks. + const auto contentInsertions = std::move(mContentInsertions); + for (const auto& entry : contentInsertions) { + mDocument->ProcessContentInserted(entry.GetKey(), entry.GetData().get()); + if (!mDocument) { + return; + } + } + + // Bind hanging child documents unless we are using IPC and the + // document has no IPC actor. If we fail to bind the child doc then + // shut it down. + uint32_t hangingDocCnt = mHangingChildDocuments.Length(); + nsTArray<RefPtr<DocAccessible>> newChildDocs; + for (uint32_t idx = 0; idx < hangingDocCnt; idx++) { + DocAccessible* childDoc = mHangingChildDocuments[idx]; + if (childDoc->IsDefunct()) { + continue; + } + + if (IPCAccessibilityActive() && !mDocument->IPCDoc()) { + childDoc->Shutdown(); + continue; + } + + nsIContent* ownerContent = childDoc->DocumentNode()->GetEmbedderElement(); + if (ownerContent) { + LocalAccessible* outerDocAcc = mDocument->GetAccessible(ownerContent); + if (outerDocAcc && outerDocAcc->AppendChild(childDoc)) { + if (mDocument->AppendChildDocument(childDoc)) { + newChildDocs.AppendElement(std::move(mHangingChildDocuments[idx])); + continue; + } + + outerDocAcc->RemoveChild(childDoc); + } + + // Failed to bind the child document, destroy it. + childDoc->Shutdown(); + } + } + + // Clear the hanging documents list, even if we didn't bind them. + mHangingChildDocuments.Clear(); + MOZ_ASSERT(mDocument, "Illicit document shutdown"); + if (!mDocument) { + return; + } + + // If the document is ready and all its subdocuments are completely loaded + // then process the document load. + if (mDocument->HasLoadState(DocAccessible::eReady) && + !mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && + hangingDocCnt == 0) { + uint32_t childDocCnt = mDocument->ChildDocumentCount(), childDocIdx = 0; + for (; childDocIdx < childDocCnt; childDocIdx++) { + DocAccessible* childDoc = mDocument->GetChildDocumentAt(childDocIdx); + if (!childDoc->HasLoadState(DocAccessible::eCompletelyLoaded)) { + break; + } + } + + if (childDocIdx == childDocCnt) { + mDocument->ProcessLoad(); + if (!mDocument) { + return; + } + } + } + + // Process invalidation list of the document after all accessible tree + // mutation is done. + mDocument->ProcessInvalidationList(); + + // Process relocation list. + for (uint32_t idx = 0; idx < mRelocations.Length(); idx++) { + // owner should be in a document and have na associated DOM node (docs + // sometimes don't) + if (mRelocations[idx]->IsInDocument() && + mRelocations[idx]->HasOwnContent()) { + mDocument->DoARIAOwnsRelocation(mRelocations[idx]); + } + } + mRelocations.Clear(); + + // Process only currently queued generic notifications. + // These are used for processing aria-activedescendant, DOMMenuItemActive, + // etc. Therefore, they must be processed after relocations, since relocated + // subtrees might not have been created before relocation processing and the + // target might be inside a relocated subtree. + const nsTArray<RefPtr<Notification>> notifications = + std::move(mNotifications); + + uint32_t notificationCount = notifications.Length(); + for (uint32_t idx = 0; idx < notificationCount; idx++) { + notifications[idx]->Process(); + if (!mDocument) { + return; + } + } + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + // If a generic notification occurs after this point then we may be allowed to + // process it synchronously. However we do not want to reenter if fireing + // events causes script to run. + mObservingState = eRefreshProcessing; + + mDocument->SendAccessiblesWillMove(); + + // Send any queued cache updates before we fire any mutation events so the + // cache is up to date when mutation events are fired. We do this after + // insertions (but not their events) so that cache updates dependent on the + // tree work correctly; e.g. line start calculation. + if (IPCAccessibilityActive() && mDocument) { + mDocument->ProcessQueuedCacheUpdates(); + } + + CoalesceMutationEvents(); + ProcessMutationEvents(); + + // When firing mutation events, mObservingState is set to + // eRefreshProcessing. Any calls to ScheduleProcessing() that + // occur before mObservingState is reset will be dropped because we only + // schedule a tick if mObservingState == eNotObservingRefresh. + // This sometimes results in our viewport cache being out-of-date after + // processing mutation events. Call ProcessQueuedCacheUpdates again to + // ensure it is updated. + if (IPCAccessibilityActive() && mDocument) { + mDocument->ProcessQueuedCacheUpdates(); + } + + if (mDocument) { + mDocument->ClearMutationData(); + } + + if (ipc::ProcessChild::ExpectingShutdown()) { + return; + } + + ProcessEventQueue(); + + if (IPCAccessibilityActive()) { + size_t newDocCount = newChildDocs.Length(); + for (size_t i = 0; i < newDocCount; i++) { + DocAccessible* childDoc = newChildDocs[i]; + if (childDoc->IsDefunct()) { + continue; + } + + LocalAccessible* parent = childDoc->LocalParent(); + DocAccessibleChild* parentIPCDoc = mDocument->IPCDoc(); + MOZ_DIAGNOSTIC_ASSERT(parentIPCDoc); + uint64_t id = reinterpret_cast<uintptr_t>(parent->UniqueID()); + MOZ_DIAGNOSTIC_ASSERT(id); + DocAccessibleChild* ipcDoc = childDoc->IPCDoc(); + if (ipcDoc) { + parentIPCDoc->SendBindChildDoc(WrapNotNull(ipcDoc), id); + continue; + } + + ipcDoc = new DocAccessibleChild(childDoc, parentIPCDoc->Manager()); + childDoc->SetIPCDoc(ipcDoc); + + nsCOMPtr<nsIBrowserChild> browserChild = + do_GetInterface(mDocument->DocumentNode()->GetDocShell()); + if (browserChild) { + static_cast<BrowserChild*>(browserChild.get()) + ->SendPDocAccessibleConstructor( + ipcDoc, parentIPCDoc, id, + childDoc->DocumentNode()->GetBrowsingContext()); + } + } + } + + if (!mDocument) { + // A null mDocument means we've gotten a Shutdown() call (presumably via + // some script that we triggered above), and that means we're done here. + // Note: in this case, it's important that don't modify mObservingState; + // Shutdown() will have *unregistered* us as a refresh observer, and we + // don't want to mistakenly overwrite mObservingState and fool ourselves + // into thinking we've re-registered when we really haven't! + MOZ_ASSERT(mObservingState == eNotObservingRefresh, + "We've been shutdown, which means we should've been " + "unregistered as a refresh observer"); + return; + } + mObservingState = eRefreshObserving; + + // Stop further processing if there are no new notifications of any kind or + // events and document load is processed. + if (mContentInsertions.Count() == 0 && mNotifications.IsEmpty() && + !mFocusEvent && mEvents.IsEmpty() && mTextArray.IsEmpty() && + mHangingChildDocuments.IsEmpty() && + mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && + mPresShell->RemoveRefreshObserver(this, FlushType::Display)) { + mObservingState = eNotObservingRefresh; + } +} + +void NotificationController::EventMap::PutEvent(AccTreeMutationEvent* aEvent) { + EventType type = GetEventType(aEvent); + uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); + MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); + addr |= type; + mTable.InsertOrUpdate(addr, RefPtr{aEvent}); +} + +AccTreeMutationEvent* NotificationController::EventMap::GetEvent( + LocalAccessible* aTarget, EventType aType) { + uint64_t addr = reinterpret_cast<uintptr_t>(aTarget); + MOZ_ASSERT((addr & 0x3) == 0, "target is not 4 byte aligned"); + + addr |= aType; + return mTable.GetWeak(addr); +} + +void NotificationController::EventMap::RemoveEvent( + AccTreeMutationEvent* aEvent) { + EventType type = GetEventType(aEvent); + uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); + MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); + addr |= type; + + MOZ_ASSERT(mTable.GetWeak(addr) == aEvent, "mTable has the wrong event"); + mTable.Remove(addr); +} + +NotificationController::EventMap::EventType +NotificationController::EventMap::GetEventType(AccTreeMutationEvent* aEvent) { + switch (aEvent->GetEventType()) { + case nsIAccessibleEvent::EVENT_SHOW: + return ShowEvent; + case nsIAccessibleEvent::EVENT_HIDE: + return HideEvent; + case nsIAccessibleEvent::EVENT_REORDER: + case nsIAccessibleEvent::EVENT_INNER_REORDER: + return ReorderEvent; + default: + MOZ_ASSERT_UNREACHABLE("event has invalid type"); + return ShowEvent; + } +} |