NotificationController.cpp (44389B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "NotificationController.h" 7 8 #include "CssAltContent.h" 9 #include "DocAccessible-inl.h" 10 #include "DocAccessibleChild.h" 11 #include "LocalAccessible-inl.h" 12 #include "nsEventShell.h" 13 #include "TextLeafAccessible.h" 14 #include "TextUpdater.h" 15 16 #include "nsIContentInlines.h" 17 18 #include "mozilla/AppShutdown.h" 19 #include "mozilla/dom/BrowserChild.h" 20 #include "mozilla/dom/Element.h" 21 #include "mozilla/PerfStats.h" 22 #include "mozilla/PresShell.h" 23 #include "mozilla/ProfilerMarkers.h" 24 #include "nsAccessibilityService.h" 25 #include "mozilla/glean/AccessibleMetrics.h" 26 27 using namespace mozilla; 28 using namespace mozilla::a11y; 29 using namespace mozilla::dom; 30 31 //////////////////////////////////////////////////////////////////////////////// 32 // NotificationCollector 33 //////////////////////////////////////////////////////////////////////////////// 34 35 NotificationController::NotificationController(DocAccessible* aDocument, 36 PresShell* aPresShell) 37 : EventQueue(aDocument), 38 mObservingState(eNotObservingRefresh), 39 mPresShell(aPresShell), 40 mEventGeneration(0) { 41 // Schedule initial accessible tree construction. 42 ScheduleProcessing(); 43 } 44 45 NotificationController::~NotificationController() { 46 NS_ASSERTION(!mDocument, "Controller wasn't shutdown properly!"); 47 if (mDocument) { 48 Shutdown(); 49 } 50 MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, 51 "Must unregister before being destroyed"); 52 } 53 54 //////////////////////////////////////////////////////////////////////////////// 55 // NotificationCollector: AddRef/Release and cycle collection 56 57 NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController) 58 NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController) 59 60 NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController) 61 62 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController) 63 if (tmp->mDocument) { 64 tmp->Shutdown(); 65 } 66 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 67 68 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController) 69 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments) 70 for (const auto& entry : tmp->mContentInsertions) { 71 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions key"); 72 cb.NoteXPCOMChild(entry.GetKey()); 73 nsTArray<nsCOMPtr<nsIContent>>* list = entry.GetData().get(); 74 for (uint32_t i = 0; i < list->Length(); i++) { 75 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mContentInsertions value item"); 76 cb.NoteXPCOMChild(list->ElementAt(i)); 77 } 78 } 79 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFocusEvent) 80 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvents) 81 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelocations) 82 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 83 84 //////////////////////////////////////////////////////////////////////////////// 85 // NotificationCollector: public 86 87 void NotificationController::Shutdown() { 88 if (mObservingState != eNotObservingRefresh && 89 mPresShell->RemoveRefreshObserver(this, FlushType::Display)) { 90 // Note, this was our last chance to unregister, since we're about to 91 // clear mPresShell further down in this function. 92 mObservingState = eNotObservingRefresh; 93 } 94 MOZ_RELEASE_ASSERT(mObservingState == eNotObservingRefresh, 95 "Must unregister before being destroyed (and we just " 96 "passed our last change to unregister)"); 97 // Immediately null out mPresShell, to prevent us from being registered as a 98 // refresh observer again. 99 mPresShell = nullptr; 100 101 // Shutdown handling child documents. 102 int32_t childDocCount = mHangingChildDocuments.Length(); 103 for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { 104 if (!mHangingChildDocuments[idx]->IsDefunct()) { 105 mHangingChildDocuments[idx]->Shutdown(); 106 } 107 } 108 109 mHangingChildDocuments.Clear(); 110 111 mDocument = nullptr; 112 113 mTextArray.Clear(); 114 mContentInsertions.Clear(); 115 mNotifications.Clear(); 116 mFocusEvent = nullptr; 117 mEvents.Clear(); 118 mRelocations.Clear(); 119 } 120 121 void NotificationController::CoalesceHideEvent(AccHideEvent* aHideEvent) { 122 LocalAccessible* parent = aHideEvent->LocalParent(); 123 while (parent) { 124 if (parent->IsDoc()) { 125 break; 126 } 127 128 if (parent->HideEventTarget()) { 129 DropMutationEvent(aHideEvent); 130 break; 131 } 132 133 if (parent->ShowEventTarget()) { 134 AccShowEvent* showEvent = 135 downcast_accEvent(mMutationMap.GetEvent(parent, EventMap::ShowEvent)); 136 if (showEvent->EventGeneration() < aHideEvent->EventGeneration()) { 137 DropMutationEvent(aHideEvent); 138 break; 139 } 140 } 141 142 parent = parent->LocalParent(); 143 } 144 } 145 146 bool NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) { 147 if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { 148 // We have to allow there to be a hide and then a show event for a target 149 // because of targets getting moved. However we need to coalesce a show and 150 // then a hide for a target which means we need to check for that here. 151 if (aEvent->GetAccessible()->ShowEventTarget()) { 152 AccTreeMutationEvent* showEvent = 153 mMutationMap.GetEvent(aEvent->GetAccessible(), EventMap::ShowEvent); 154 DropMutationEvent(showEvent); 155 return false; 156 } 157 158 // Don't queue a hide event on an accessible that's already being moved. It 159 // or an ancestor should already have a hide event queued. 160 if (mDocument && 161 mDocument->IsAccessibleBeingMoved(aEvent->GetAccessible())) { 162 return false; 163 } 164 165 // If this is an additional hide event, the accessible may be hidden, or 166 // moved again after a move. Preserve the original hide event since 167 // its properties are consistent with the tree that existed before 168 // the next batch of mutation events is processed. 169 if (aEvent->GetAccessible()->HideEventTarget()) { 170 return false; 171 } 172 } 173 174 AccMutationEvent* mutEvent = downcast_accEvent(aEvent); 175 mEventGeneration++; 176 mutEvent->SetEventGeneration(mEventGeneration); 177 178 if (!mFirstMutationEvent) { 179 mFirstMutationEvent = aEvent; 180 ScheduleProcessing(); 181 } 182 183 if (mLastMutationEvent) { 184 NS_ASSERTION(!mLastMutationEvent->NextEvent(), 185 "why isn't the last event the end?"); 186 mLastMutationEvent->SetNextEvent(aEvent); 187 } 188 189 aEvent->SetPrevEvent(mLastMutationEvent); 190 mLastMutationEvent = aEvent; 191 mMutationMap.PutEvent(aEvent); 192 193 // Because we could be hiding the target of a show event we need to get rid 194 // of any such events. 195 if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE) { 196 CoalesceHideEvent(downcast_accEvent(aEvent)); 197 198 // mLastMutationEvent will point to something other than aEvent if and only 199 // if aEvent was just coalesced away. In that case a parent accessible 200 // must already have the required reorder and text change events so we are 201 // done here. 202 if (mLastMutationEvent != aEvent) { 203 return false; 204 } 205 } 206 207 if (aEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE || 208 aEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { 209 LocalAccessible* target = aEvent->GetAccessible(); 210 // We need to do this here while the relation is still intact. During the 211 // tick, where we we call PushNameOrDescriptionChange, it will be too late 212 // since we will already have unparented the label and severed the relation. 213 if (PushNameOrDescriptionChangeToRelations(target, 214 RelationType::LABEL_FOR) || 215 PushNameOrDescriptionChangeToRelations(target, 216 RelationType::DESCRIPTION_FOR)) { 217 ScheduleProcessing(); 218 } 219 } 220 221 // We need to fire a reorder event after all of the events targeted at shown 222 // or hidden children of a container. So either queue a new one, or move an 223 // existing one to the end of the queue if the container already has a 224 // reorder event. 225 LocalAccessible* container = aEvent->GetAccessible()->LocalParent(); 226 RefPtr<AccReorderEvent> reorder; 227 if (!container->ReorderEventTarget()) { 228 reorder = new AccReorderEvent(container); 229 container->SetReorderEventTarget(true); 230 mMutationMap.PutEvent(reorder); 231 } else { 232 AccReorderEvent* event = downcast_accEvent( 233 mMutationMap.GetEvent(container, EventMap::ReorderEvent)); 234 reorder = event; 235 if (mFirstMutationEvent == event) { 236 mFirstMutationEvent = event->NextEvent(); 237 } else { 238 event->PrevEvent()->SetNextEvent(event->NextEvent()); 239 } 240 241 event->NextEvent()->SetPrevEvent(event->PrevEvent()); 242 event->SetNextEvent(nullptr); 243 } 244 245 reorder->SetEventGeneration(mEventGeneration); 246 reorder->SetPrevEvent(mLastMutationEvent); 247 mLastMutationEvent->SetNextEvent(reorder); 248 mLastMutationEvent = reorder; 249 250 // It is not possible to have a text change event for something other than a 251 // hyper text accessible. 252 if (!container->IsHyperText()) { 253 return true; 254 } 255 256 MOZ_ASSERT(mutEvent); 257 258 nsString text; 259 aEvent->GetAccessible()->AppendTextTo(text); 260 if (text.IsEmpty()) { 261 return true; 262 } 263 264 LocalAccessible* target = aEvent->GetAccessible(); 265 int32_t offset = container->AsHyperText()->GetChildOffset(target); 266 AccTreeMutationEvent* prevEvent = aEvent->PrevEvent(); 267 while (prevEvent && 268 prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { 269 prevEvent = prevEvent->PrevEvent(); 270 } 271 272 if (prevEvent && 273 prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_HIDE && 274 mutEvent->IsHide()) { 275 AccHideEvent* prevHide = downcast_accEvent(prevEvent); 276 AccTextChangeEvent* prevTextChange = prevHide->mTextChangeEvent; 277 if (prevTextChange && prevHide->LocalParent() == mutEvent->LocalParent()) { 278 if (prevHide->mNextSibling == target) { 279 target->AppendTextTo(prevTextChange->mModifiedText); 280 prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); 281 } else if (prevHide->mPrevSibling == target) { 282 nsString temp; 283 target->AppendTextTo(temp); 284 285 uint32_t extraLen = temp.Length(); 286 temp += prevTextChange->mModifiedText; 287 ; 288 prevTextChange->mModifiedText = temp; 289 prevTextChange->mStart -= extraLen; 290 prevHide->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); 291 } 292 } 293 } else if (prevEvent && mutEvent->IsShow() && 294 prevEvent->GetEventType() == nsIAccessibleEvent::EVENT_SHOW) { 295 AccShowEvent* prevShow = downcast_accEvent(prevEvent); 296 AccTextChangeEvent* prevTextChange = prevShow->mTextChangeEvent; 297 if (prevTextChange && prevShow->LocalParent() == target->LocalParent()) { 298 int32_t index = target->IndexInParent(); 299 int32_t prevIndex = prevShow->GetAccessible()->IndexInParent(); 300 if (prevIndex + 1 == index) { 301 target->AppendTextTo(prevTextChange->mModifiedText); 302 prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); 303 } else if (index + 1 == prevIndex) { 304 nsString temp; 305 target->AppendTextTo(temp); 306 prevTextChange->mStart -= temp.Length(); 307 temp += prevTextChange->mModifiedText; 308 prevTextChange->mModifiedText = temp; 309 prevShow->mTextChangeEvent.swap(mutEvent->mTextChangeEvent); 310 } 311 } 312 } 313 314 if (!mutEvent->mTextChangeEvent) { 315 mutEvent->mTextChangeEvent = new AccTextChangeEvent( 316 container, offset, text, mutEvent->IsShow(), 317 aEvent->mIsFromUserInput ? eFromUserInput : eNoUserInput); 318 } 319 320 return true; 321 } 322 323 void NotificationController::DropMutationEvent(AccTreeMutationEvent* aEvent) { 324 const uint32_t eventType = aEvent->GetEventType(); 325 MOZ_ASSERT(eventType != nsIAccessibleEvent::EVENT_INNER_REORDER, 326 "Inner reorder has already been dropped, cannot drop again"); 327 if (eventType == nsIAccessibleEvent::EVENT_REORDER) { 328 // We don't fully drop reorder events, we just change them to inner reorder 329 // events. 330 AccReorderEvent* reorderEvent = downcast_accEvent(aEvent); 331 332 MOZ_ASSERT(reorderEvent); 333 reorderEvent->SetInner(); 334 return; 335 } 336 if (eventType == nsIAccessibleEvent::EVENT_SHOW) { 337 // unset the event bits since the event isn't being fired any more. 338 aEvent->GetAccessible()->SetShowEventTarget(false); 339 } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) { 340 // unset the event bits since the event isn't being fired any more. 341 aEvent->GetAccessible()->SetHideEventTarget(false); 342 343 AccHideEvent* hideEvent = downcast_accEvent(aEvent); 344 MOZ_ASSERT(hideEvent); 345 346 if (hideEvent->NeedsShutdown()) { 347 mDocument->ShutdownChildrenInSubtree(aEvent->GetAccessible()); 348 } 349 } else { 350 MOZ_ASSERT_UNREACHABLE("Mutation event has non-mutation event type"); 351 } 352 353 // Do the work to splice the event out of the list. 354 if (mFirstMutationEvent == aEvent) { 355 mFirstMutationEvent = aEvent->NextEvent(); 356 } else { 357 aEvent->PrevEvent()->SetNextEvent(aEvent->NextEvent()); 358 } 359 360 if (mLastMutationEvent == aEvent) { 361 mLastMutationEvent = aEvent->PrevEvent(); 362 } else { 363 aEvent->NextEvent()->SetPrevEvent(aEvent->PrevEvent()); 364 } 365 366 aEvent->SetPrevEvent(nullptr); 367 aEvent->SetNextEvent(nullptr); 368 mMutationMap.RemoveEvent(aEvent); 369 } 370 371 void NotificationController::CoalesceMutationEvents() { 372 AUTO_PROFILER_MARKER_TEXT("NotificationController::CoalesceMutationEvents", 373 A11Y, {}, ""_ns); 374 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_CoalesceMutationEvents> 375 autoRecording; 376 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 377 378 AccTreeMutationEvent* event = mFirstMutationEvent; 379 while (event) { 380 AccTreeMutationEvent* nextEvent = event->NextEvent(); 381 uint32_t eventType = event->GetEventType(); 382 if (event->GetEventType() == nsIAccessibleEvent::EVENT_REORDER) { 383 LocalAccessible* acc = event->GetAccessible(); 384 while (acc) { 385 if (acc->IsDoc()) { 386 break; 387 } 388 389 // if a parent of the reorder event's target is being hidden that 390 // hide event's target must have a parent that is also a reorder event 391 // target. That means we don't need this reorder event. 392 if (acc->HideEventTarget()) { 393 DropMutationEvent(event); 394 break; 395 } 396 397 LocalAccessible* parent = acc->LocalParent(); 398 if (parent && parent->ReorderEventTarget()) { 399 AccReorderEvent* reorder = downcast_accEvent( 400 mMutationMap.GetEvent(parent, EventMap::ReorderEvent)); 401 402 // We want to make sure that a reorder event comes after any show or 403 // hide events targeted at the children of its target. We keep the 404 // invariant that event generation goes up as you are farther in the 405 // queue, so we want to use the spot of the event with the higher 406 // generation number, and keep that generation number. 407 if (reorder && 408 reorder->EventGeneration() < event->EventGeneration()) { 409 reorder->SetEventGeneration(event->EventGeneration()); 410 411 // It may be true that reorder was before event, and we coalesced 412 // away all the show / hide events between them. In that case 413 // event is already immediately after reorder in the queue and we 414 // do not need to rearrange the list of events. 415 if (event != reorder->NextEvent()) { 416 // There really should be a show or hide event before the first 417 // reorder event. 418 if (reorder->PrevEvent()) { 419 reorder->PrevEvent()->SetNextEvent(reorder->NextEvent()); 420 } else { 421 mFirstMutationEvent = reorder->NextEvent(); 422 } 423 424 reorder->NextEvent()->SetPrevEvent(reorder->PrevEvent()); 425 event->PrevEvent()->SetNextEvent(reorder); 426 reorder->SetPrevEvent(event->PrevEvent()); 427 event->SetPrevEvent(reorder); 428 reorder->SetNextEvent(event); 429 } 430 } 431 DropMutationEvent(event); 432 break; 433 } 434 435 acc = parent; 436 } 437 } else if (eventType == nsIAccessibleEvent::EVENT_SHOW) { 438 LocalAccessible* parent = event->GetAccessible()->LocalParent(); 439 while (parent) { 440 if (parent->IsDoc()) { 441 break; 442 } 443 444 // if the parent of a show event is being either shown or hidden then 445 // we don't need to fire a show event for a subtree of that change. 446 if (parent->ShowEventTarget() || parent->HideEventTarget()) { 447 DropMutationEvent(event); 448 break; 449 } 450 451 parent = parent->LocalParent(); 452 } 453 } else if (eventType == nsIAccessibleEvent::EVENT_HIDE) { 454 MOZ_ASSERT(eventType == nsIAccessibleEvent::EVENT_HIDE, 455 "mutation event list has an invalid event"); 456 457 AccHideEvent* hideEvent = downcast_accEvent(event); 458 CoalesceHideEvent(hideEvent); 459 } 460 461 event = nextEvent; 462 } 463 } 464 465 void NotificationController::ScheduleChildDocBinding(DocAccessible* aDocument) { 466 // Schedule child document binding to the tree. 467 mHangingChildDocuments.AppendElement(aDocument); 468 ScheduleProcessing(); 469 } 470 471 void NotificationController::ScheduleContentInsertion( 472 LocalAccessible* aContainer, nsTArray<nsCOMPtr<nsIContent>>& aInsertions) { 473 if (!aInsertions.IsEmpty()) { 474 mContentInsertions.GetOrInsertNew(aContainer)->AppendElements(aInsertions); 475 ScheduleProcessing(); 476 } 477 } 478 479 void NotificationController::ScheduleProcessing() { 480 // If notification flush isn't planned yet, start notification flush 481 // asynchronously (after style and layout). 482 // Note: the mPresShell null-check might be unnecessary; it's just to prevent 483 // a null-deref here, if we somehow get called after we've been shut down. 484 if (mObservingState == eNotObservingRefresh && mPresShell) { 485 if (mPresShell->AddRefreshObserver(this, FlushType::Display, 486 "Accessibility notifications")) { 487 mObservingState = eRefreshObserving; 488 } 489 } 490 } 491 492 //////////////////////////////////////////////////////////////////////////////// 493 // NotificationCollector: protected 494 495 bool NotificationController::IsUpdatePending() const { 496 return mPresShell->NeedStyleFlush() || mPresShell->NeedLayoutFlush() || 497 mObservingState == eRefreshProcessingForUpdate || WaitingForParent() || 498 mContentInsertions.Count() != 0 || mNotifications.Length() != 0 || 499 !mTextArray.IsEmpty() || 500 !mDocument->HasLoadState(DocAccessible::eTreeConstructed); 501 } 502 503 bool NotificationController::WaitingForParent() const { 504 DocAccessible* parentdoc = mDocument->ParentDocument(); 505 if (!parentdoc) { 506 return false; 507 } 508 509 NotificationController* parent = parentdoc->mNotificationController; 510 if (!parent || parent == this) { 511 // Do not wait for nothing or ourselves 512 return false; 513 } 514 515 // Wait for parent's notifications processing 516 return parent->mContentInsertions.Count() != 0 || 517 parent->mNotifications.Length() != 0; 518 } 519 520 void NotificationController::ProcessMutationEvents() { 521 // Firing an event can indirectly run script; e.g. an XPCOM event observer 522 // or querying a XUL interface. Further mutations might be queued as a result. 523 // It's important that the mutation queue and state bits from one tick don't 524 // interfere with the next tick. Otherwise, we can end up dropping events. 525 // Therefore: 526 // 1. Clear the state bits, which we only need for coalescence. 527 for (AccTreeMutationEvent* event = mFirstMutationEvent; event; 528 event = event->NextEvent()) { 529 LocalAccessible* acc = event->GetAccessible(); 530 acc->SetShowEventTarget(false); 531 acc->SetHideEventTarget(false); 532 acc->SetReorderEventTarget(false); 533 } 534 // 2. Keep the current queue locally, but clear the queue on the instance. 535 RefPtr<AccTreeMutationEvent> firstEvent = mFirstMutationEvent; 536 mFirstMutationEvent = mLastMutationEvent = nullptr; 537 mMutationMap.Clear(); 538 mEventGeneration = 0; 539 540 // Group the show events by the parent of their target. 541 nsTHashMap<nsPtrHashKey<LocalAccessible>, nsTArray<AccTreeMutationEvent*>> 542 showEvents; 543 for (AccTreeMutationEvent* event = firstEvent; event; 544 event = event->NextEvent()) { 545 if (event->GetEventType() != nsIAccessibleEvent::EVENT_SHOW) { 546 continue; 547 } 548 549 LocalAccessible* parent = event->GetAccessible()->LocalParent(); 550 showEvents.LookupOrInsert(parent).AppendElement(event); 551 } 552 553 // We need to fire show events for the children of an accessible in the order 554 // of their indices at this point. So sort each set of events for the same 555 // container by the index of their target. We do this before firing any events 556 // because firing an event might indirectly run script which might alter the 557 // tree, breaking our sort. However, we don't actually fire the events yet. 558 for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { 559 struct AccIdxComparator { 560 bool LessThan(const AccTreeMutationEvent* a, 561 const AccTreeMutationEvent* b) const { 562 int32_t aIdx = a->GetAccessible()->IndexInParent(); 563 int32_t bIdx = b->GetAccessible()->IndexInParent(); 564 MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && (a == b || aIdx != bIdx)); 565 return aIdx < bIdx; 566 } 567 bool Equals(const AccTreeMutationEvent* a, 568 const AccTreeMutationEvent* b) const { 569 DebugOnly<int32_t> aIdx = a->GetAccessible()->IndexInParent(); 570 DebugOnly<int32_t> bIdx = b->GetAccessible()->IndexInParent(); 571 MOZ_ASSERT(aIdx >= 0 && bIdx >= 0 && (a == b || aIdx != bIdx)); 572 return a == b; 573 } 574 }; 575 576 nsTArray<AccTreeMutationEvent*>& events = iter.Data(); 577 events.Sort(AccIdxComparator()); 578 } 579 580 // there is no reason to fire a hide event for a child of a show event 581 // target. That can happen if something is inserted into the tree and 582 // removed before the next refresh driver tick, but it should not be 583 // observable outside gecko so it should be safe to coalesce away any such 584 // events. This means that it should be fine to fire all of the hide events 585 // first, and then deal with any shown subtrees. 586 for (AccTreeMutationEvent* event = firstEvent; event; 587 event = event->NextEvent()) { 588 if (event->GetEventType() != nsIAccessibleEvent::EVENT_HIDE) { 589 continue; 590 } 591 592 nsEventShell::FireEvent(event); 593 if (!mDocument) { 594 return; 595 } 596 597 AccMutationEvent* mutEvent = downcast_accEvent(event); 598 if (mutEvent->mTextChangeEvent) { 599 nsEventShell::FireEvent(mutEvent->mTextChangeEvent); 600 if (!mDocument) { 601 return; 602 } 603 } 604 605 AccHideEvent* hideEvent = downcast_accEvent(event); 606 if (hideEvent->NeedsShutdown()) { 607 mDocument->ShutdownChildrenInSubtree(event->mAccessible); 608 } 609 } 610 611 // Fire the show events we sorted earlier. 612 for (auto iter = showEvents.Iter(); !iter.Done(); iter.Next()) { 613 nsTArray<AccTreeMutationEvent*>& events = iter.Data(); 614 for (AccTreeMutationEvent* event : events) { 615 nsEventShell::FireEvent(event); 616 if (!mDocument) { 617 return; 618 } 619 620 AccMutationEvent* mutEvent = downcast_accEvent(event); 621 if (mutEvent->mTextChangeEvent) { 622 nsEventShell::FireEvent(mutEvent->mTextChangeEvent); 623 if (!mDocument) { 624 return; 625 } 626 } 627 } 628 } 629 630 // Now we can fire the reorder events after all the show and hide events. 631 for (const uint32_t reorderType : {nsIAccessibleEvent::EVENT_INNER_REORDER, 632 nsIAccessibleEvent::EVENT_REORDER}) { 633 for (AccTreeMutationEvent* event = firstEvent; event; 634 event = event->NextEvent()) { 635 if (event->GetEventType() != reorderType) { 636 continue; 637 } 638 639 if (event->GetAccessible()->IsDefunct()) { 640 // An inner reorder target may have been hidden itself and no 641 // longer bound to the document. 642 MOZ_ASSERT(reorderType == nsIAccessibleEvent::EVENT_INNER_REORDER, 643 "An 'outer' reorder target should not be defunct"); 644 continue; 645 } 646 647 nsEventShell::FireEvent(event); 648 if (!mDocument) { 649 return; 650 } 651 652 // The mutation in the container can change its name, or an ancestor's 653 // name. A labelled/described by relation would also need to be notified 654 // if this is the case. 655 if (PushNameOrDescriptionChange(event)) { 656 ScheduleProcessing(); 657 } 658 659 LocalAccessible* target = event->GetAccessible(); 660 target->Document()->MaybeNotifyOfValueChange(target); 661 if (!mDocument) { 662 return; 663 } 664 } 665 } 666 667 // Our events are in a doubly linked list. Clear the pointers to reduce 668 // pressure on the cycle collector. Even though clearing the previous pointers 669 // removes cycles, this isn't enough. The cycle collector still gets bogged 670 // down when there are lots of mutation events if the next pointers aren't 671 // cleared. Even without the cycle collector, not clearing the next pointers 672 // potentially results in deep recursion because releasing each event releases 673 // its next event. 674 RefPtr<AccTreeMutationEvent> event = firstEvent; 675 while (event) { 676 RefPtr<AccTreeMutationEvent> next = event->NextEvent(); 677 event->SetNextEvent(nullptr); 678 event->SetPrevEvent(nullptr); 679 event = next; 680 } 681 } 682 683 //////////////////////////////////////////////////////////////////////////////// 684 // NotificationCollector: private 685 686 void NotificationController::WillRefresh(mozilla::TimeStamp aTime) { 687 AUTO_PROFILER_MARKER_UNTYPED("NotificationController::WillRefresh", A11Y, {}); 688 689 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_WillRefresh> 690 autoRecording; 691 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 692 auto timer = glean::a11y::tree_update_timing.Measure(); 693 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 694 695 AUTO_PROFILER_LABEL("NotificationController::WillRefresh", A11Y); 696 697 // If mDocument is null, the document accessible that this notification 698 // controller was created for is now shut down. This means we've lost our 699 // ability to unregister ourselves, which is bad. (However, it also shouldn't 700 // be logically possible for us to get here with a null mDocument; the only 701 // thing that clears that pointer is our Shutdown() method, which first 702 // unregisters and fatally asserts if that fails). 703 MOZ_RELEASE_ASSERT( 704 mDocument, 705 "The document was shut down while refresh observer is attached!"); 706 707 if (AppShutdown::IsShutdownImpending()) { 708 return; 709 } 710 711 // Wait until an update, we have started, or an interruptible reflow is 712 // finished. We also check the existance of our pres context and root pres 713 // context, since if we can't reach either of these the frame tree is being 714 // destroyed. 715 nsPresContext* pc = mPresShell->GetPresContext(); 716 if (mObservingState == eRefreshProcessing || 717 mObservingState == eRefreshProcessingForUpdate || 718 mPresShell->IsReflowInterrupted() || !pc || !pc->GetRootPresContext()) { 719 return; 720 } 721 722 if (mDocument->IPCDoc() && mDocument->IPCDoc()->HasUnackedMutationEvents()) { 723 // We've sent mutation events to the parent process, but we haven't 724 // received its ACK yet. We defer accessibility updates until we do. 725 // Otherwise, we might flood the IPDL queue with many later mutation events 726 // while the parent process is still trying to process earlier ones, getting 727 // further and further behind and causing the browser to hang for extended 728 // periods. If the same nodes are repeatedly changing or being recreated, 729 // deferring updates can significantly reduce the overall number of events 730 // because we avoid generating events for the intermediate changes that 731 // occur while the parent process is busy. This also avoids the associated 732 // work to update the tree in the content process. We must defer all 733 // work here, not just mutation events, because otherwise, the tree and 734 // other events might get out of sync with the mutation events we've 735 // processed. Queued content insertions, events, etc. will be processed in 736 // a subsequent tick after we receive the ACK, though some of them may be 737 // irrelevant (and thus dropped) by the time that happens if a DOM node or 738 // Accessible was removed in the interim. 739 return; 740 } 741 742 // Process parent's notifications before ours, to get proper ordering between 743 // e.g. tab event and content event. 744 if (WaitingForParent()) { 745 mDocument->ParentDocument()->mNotificationController->WillRefresh(aTime); 746 if (!mDocument || AppShutdown::IsShutdownImpending()) { 747 return; 748 } 749 } 750 751 // Any generic notifications should be queued if we're processing content 752 // insertions or generic notifications. 753 mObservingState = eRefreshProcessingForUpdate; 754 755 // Initial accessible tree construction. 756 if (!mDocument->HasLoadState(DocAccessible::eTreeConstructed)) { 757 // (1) If document is not bound to parent at this point, or 758 // (2) the PresShell is not initialized (and it isn't about:blank), 759 // then the document is not ready yet (process notifications later). 760 if (!mDocument->IsBoundToParent() || 761 (!mPresShell->DidInitialize() && 762 !mDocument->DocumentNode()->IsInitialDocument())) { 763 mObservingState = eRefreshObserving; 764 return; 765 } 766 767 #ifdef A11Y_LOG 768 if (logging::IsEnabled(logging::eTree)) { 769 logging::MsgBegin("TREE", "initial tree created"); 770 logging::Address("document", mDocument); 771 logging::MsgEnd(); 772 } 773 #endif 774 775 mDocument->DoInitialUpdate(); 776 if (AppShutdown::IsShutdownImpending()) { 777 return; 778 } 779 780 NS_ASSERTION(mContentInsertions.Count() == 0, 781 "Pending content insertions while initial accessible tree " 782 "isn't created!"); 783 } 784 785 mDocument->ProcessPendingUpdates(); 786 787 // Process rendered text change notifications. Even though we want to process 788 // them in the order in which they were queued, we still want to avoid 789 // duplicates. 790 nsTHashSet<nsIContent*> textHash; 791 for (nsIContent* textNode : mTextArray) { 792 if (!textHash.EnsureInserted(textNode)) { 793 continue; // Already processed. 794 } 795 LocalAccessible* textAcc = mDocument->GetAccessible(textNode); 796 797 // If the text node is not in tree or doesn't have a frame, or placed in 798 // another document, then this case should have been handled already by 799 // content removal notifications. 800 nsINode* containerNode = textNode->GetFlattenedTreeParentNode(); 801 if (!containerNode || textNode->OwnerDoc() != mDocument->DocumentNode()) { 802 MOZ_ASSERT(!textAcc, 803 "Text node was removed but accessible is kept alive!"); 804 continue; 805 } 806 807 nsIFrame* textFrame = textNode->GetPrimaryFrame(); 808 if (!textFrame) { 809 MOZ_ASSERT(!textAcc, 810 "Text node isn't rendered but accessible is kept alive!"); 811 continue; 812 } 813 814 #ifdef A11Y_LOG 815 nsIContent* containerElm = 816 containerNode->IsElement() ? containerNode->AsElement() : nullptr; 817 #endif 818 819 nsIFrame::RenderedText text = textFrame->GetRenderedText( 820 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, 821 nsIFrame::TrailingWhitespace::DontTrim); 822 823 if (textAcc) { 824 // Remove the TextLeafAccessible if: 825 // 1. The rendered text is empty; or 826 // 2. The text is invisible, semantically irrelevant whitespace before a 827 // hard line break. 828 if (text.mString.IsEmpty() || 829 nsCoreUtils::IsTrimmedWhitespaceBeforeHardLineBreak(textFrame)) { 830 #ifdef A11Y_LOG 831 if (logging::IsEnabled(logging::eTree | logging::eText)) { 832 logging::MsgBegin("TREE", "text node lost its content; doc: %p", 833 mDocument); 834 logging::Node("container", containerElm); 835 logging::Node("content", textNode); 836 logging::MsgEnd(); 837 } 838 #endif 839 840 mDocument->ContentRemoved(textAcc); 841 continue; 842 } 843 844 // Update text of the accessible and fire text change events. 845 #ifdef A11Y_LOG 846 if (logging::IsEnabled(logging::eText)) { 847 logging::MsgBegin("TEXT", "text may be changed; doc: %p", mDocument); 848 logging::Node("container", containerElm); 849 logging::Node("content", textNode); 850 logging::MsgEntry( 851 "old text '%s'", 852 NS_ConvertUTF16toUTF8(textAcc->AsTextLeaf()->Text()).get()); 853 logging::MsgEntry("new text: '%s'", 854 NS_ConvertUTF16toUTF8(text.mString).get()); 855 logging::MsgEnd(); 856 } 857 #endif 858 859 if (CssAltContent(textNode)) { 860 // A11y doesn't care about the text rendered by layout if there is CSS 861 // content alt text. We skip this here rather than when the update is 862 // queued because the TextLeafAccessible might not exist yet and we 863 // might need to create it below. 864 continue; 865 } 866 867 TextUpdater::Run(mDocument, textAcc->AsTextLeaf(), text.mString); 868 continue; 869 } 870 871 // Append an accessible if rendered text is not empty. 872 if (!text.mString.IsEmpty()) { 873 #ifdef A11Y_LOG 874 if (logging::IsEnabled(logging::eTree | logging::eText)) { 875 logging::MsgBegin("TREE", "text node gains new content; doc: %p", 876 mDocument); 877 logging::Node("container", containerElm); 878 logging::Node("content", textNode); 879 logging::MsgEnd(); 880 } 881 #endif 882 883 MOZ_ASSERT(mDocument->AccessibleOrTrueContainer(containerNode), 884 "Text node having rendered text hasn't accessible document!"); 885 886 LocalAccessible* container = 887 mDocument->AccessibleOrTrueContainer(containerNode, true); 888 if (container) { 889 nsTArray<nsCOMPtr<nsIContent>>* list = 890 mContentInsertions.GetOrInsertNew(container); 891 list->AppendElement(textNode); 892 } 893 } 894 } 895 textHash.Clear(); 896 mTextArray.Clear(); 897 898 // Process content inserted notifications to update the tree. 899 // Processing an insertion can indirectly run script (e.g. querying a XUL 900 // interface), which might result in another insertion being queued. 901 // We don't want to lose any queued insertions if this happens. Therefore, we 902 // move the current insertions into a temporary data structure and process 903 // them from there. Any insertions queued during processing will get handled 904 // in subsequent refresh driver ticks. 905 const auto contentInsertions = std::move(mContentInsertions); 906 for (const auto& entry : contentInsertions) { 907 mDocument->ProcessContentInserted(entry.GetKey(), entry.GetData().get()); 908 if (!mDocument) { 909 return; 910 } 911 } 912 913 // Bind hanging child documents unless we are using IPC and the 914 // document has no IPC actor. If we fail to bind the child doc then 915 // shut it down. 916 uint32_t hangingDocCnt = mHangingChildDocuments.Length(); 917 nsTArray<RefPtr<DocAccessible>> newChildDocs; 918 for (uint32_t idx = 0; idx < hangingDocCnt; idx++) { 919 DocAccessible* childDoc = mHangingChildDocuments[idx]; 920 if (childDoc->IsDefunct()) { 921 continue; 922 } 923 924 if (IPCAccessibilityActive() && !mDocument->IPCDoc()) { 925 childDoc->Shutdown(); 926 continue; 927 } 928 929 nsIContent* ownerContent = childDoc->DocumentNode()->GetEmbedderElement(); 930 if (ownerContent) { 931 LocalAccessible* outerDocAcc = mDocument->GetAccessible(ownerContent); 932 if (outerDocAcc && outerDocAcc->AppendChild(childDoc)) { 933 if (mDocument->AppendChildDocument(childDoc)) { 934 newChildDocs.AppendElement(std::move(mHangingChildDocuments[idx])); 935 continue; 936 } 937 938 outerDocAcc->RemoveChild(childDoc); 939 } 940 941 // Failed to bind the child document, destroy it. 942 childDoc->Shutdown(); 943 } 944 } 945 946 // Clear the hanging documents list, even if we didn't bind them. 947 mHangingChildDocuments.Clear(); 948 MOZ_ASSERT(mDocument, "Illicit document shutdown"); 949 if (!mDocument) { 950 return; 951 } 952 953 // If the document is ready and all its subdocuments are completely loaded 954 // then process the document load. 955 if (mDocument->HasLoadState(DocAccessible::eReady) && 956 !mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && 957 hangingDocCnt == 0) { 958 uint32_t childDocCnt = mDocument->ChildDocumentCount(), childDocIdx = 0; 959 for (; childDocIdx < childDocCnt; childDocIdx++) { 960 DocAccessible* childDoc = mDocument->GetChildDocumentAt(childDocIdx); 961 if (!childDoc->HasLoadState(DocAccessible::eCompletelyLoaded)) { 962 break; 963 } 964 } 965 966 if (childDocIdx == childDocCnt) { 967 mDocument->ProcessLoad(); 968 if (!mDocument) { 969 return; 970 } 971 } 972 } 973 974 // Process invalidation list of the document after all accessible tree 975 // mutation is done. 976 mDocument->ProcessInvalidationList(); 977 978 // Process relocation list. 979 for (uint32_t idx = 0; idx < mRelocations.Length(); idx++) { 980 // owner should be in a document and have na associated DOM node (docs 981 // sometimes don't) 982 if (mRelocations[idx]->IsInDocument() && 983 mRelocations[idx]->HasOwnContent()) { 984 mDocument->DoARIAOwnsRelocation(mRelocations[idx]); 985 } 986 } 987 mRelocations.Clear(); 988 989 // Process only currently queued generic notifications. 990 // These are used for processing aria-activedescendant, DOMMenuItemActive, 991 // etc. Therefore, they must be processed after relocations, since relocated 992 // subtrees might not have been created before relocation processing and the 993 // target might be inside a relocated subtree. 994 const nsTArray<RefPtr<Notification>> notifications = 995 std::move(mNotifications); 996 997 uint32_t notificationCount = notifications.Length(); 998 for (uint32_t idx = 0; idx < notificationCount; idx++) { 999 notifications[idx]->Process(); 1000 if (!mDocument) { 1001 return; 1002 } 1003 } 1004 1005 if (AppShutdown::IsShutdownImpending()) { 1006 return; 1007 } 1008 1009 // If a generic notification occurs after this point then we may be allowed to 1010 // process it synchronously. However we do not want to reenter if fireing 1011 // events causes script to run. 1012 mObservingState = eRefreshProcessing; 1013 1014 mDocument->SendAccessiblesWillMove(); 1015 1016 // Send any queued cache updates before we fire any mutation events so the 1017 // cache is up to date when mutation events are fired. We do this after 1018 // insertions (but not their events) so that cache updates dependent on the 1019 // tree work correctly; e.g. line start calculation. 1020 if (IPCAccessibilityActive() && mDocument) { 1021 mDocument->ProcessQueuedCacheUpdates(); 1022 } 1023 1024 CoalesceMutationEvents(); 1025 ProcessMutationEvents(); 1026 1027 // ProcessMutationEvents for content process documents merely queues mutation 1028 // events. Send those events in a batch now if applicable. 1029 if (mDocument && mDocument->IPCDoc()) { 1030 mDocument->IPCDoc()->SendQueuedMutationEvents(); 1031 } 1032 1033 // When firing mutation events, mObservingState is set to 1034 // eRefreshProcessing. Any calls to ScheduleProcessing() that 1035 // occur before mObservingState is reset will be dropped because we only 1036 // schedule a tick if mObservingState == eNotObservingRefresh. 1037 // This sometimes results in our viewport cache being out-of-date after 1038 // processing mutation events. Call ProcessQueuedCacheUpdates again to 1039 // ensure it is updated. 1040 if (IPCAccessibilityActive() && mDocument) { 1041 mDocument->ProcessQueuedCacheUpdates(); 1042 } 1043 1044 if (mDocument) { 1045 mDocument->ClearMutationData(); 1046 } 1047 1048 if (AppShutdown::IsShutdownImpending()) { 1049 return; 1050 } 1051 1052 ProcessEventQueue(); 1053 if (mDocument) { 1054 // Process an anchor jump (if any) now that the tree and focus are up to 1055 // date. 1056 mDocument->ProcessAnchorJump(); 1057 } 1058 1059 if (mDocument && mDocument->IPCDoc()) { 1060 // There should not be any more mutation events in the mutation event queue. 1061 // ProcessEventQueue should have sent all of them. 1062 MOZ_ASSERT(mDocument->IPCDoc()->MutationEventQueueLength() == 0, 1063 "Mutation event queue is non-empty."); 1064 if (mDocument->IPCDoc()->HasUnackedMutationEvents()) { 1065 // Now that all mutation events have been sent, request an ACK from the 1066 // parent process. This request will be after the mutation events in the 1067 // IPDL queue, so the parent process will respond once it has finished 1068 // handling all the mutation events. 1069 (void)mDocument->IPCDoc()->SendRequestAckMutationEvents(); 1070 } 1071 } 1072 1073 if (IPCAccessibilityActive()) { 1074 size_t newDocCount = newChildDocs.Length(); 1075 for (size_t i = 0; i < newDocCount; i++) { 1076 DocAccessible* childDoc = newChildDocs[i]; 1077 if (childDoc->IsDefunct()) { 1078 continue; 1079 } 1080 1081 LocalAccessible* parent = childDoc->LocalParent(); 1082 DocAccessibleChild* parentIPCDoc = mDocument->IPCDoc(); 1083 MOZ_DIAGNOSTIC_ASSERT(parentIPCDoc); 1084 uint64_t id = reinterpret_cast<uintptr_t>(parent->UniqueID()); 1085 MOZ_DIAGNOSTIC_ASSERT(id); 1086 DocAccessibleChild* ipcDoc = childDoc->IPCDoc(); 1087 if (ipcDoc) { 1088 parentIPCDoc->SendBindChildDoc(WrapNotNull(ipcDoc), id); 1089 continue; 1090 } 1091 1092 ipcDoc = new DocAccessibleChild(childDoc, parentIPCDoc->Manager()); 1093 childDoc->SetIPCDoc(ipcDoc); 1094 1095 nsCOMPtr<nsIBrowserChild> browserChild = 1096 do_GetInterface(mDocument->DocumentNode()->GetDocShell()); 1097 if (browserChild) { 1098 static_cast<BrowserChild*>(browserChild.get()) 1099 ->SendPDocAccessibleConstructor( 1100 ipcDoc, parentIPCDoc, id, 1101 childDoc->DocumentNode()->GetBrowsingContext()); 1102 } 1103 } 1104 } 1105 1106 if (!mDocument) { 1107 // A null mDocument means we've gotten a Shutdown() call (presumably via 1108 // some script that we triggered above), and that means we're done here. 1109 // Note: in this case, it's important that don't modify mObservingState; 1110 // Shutdown() will have *unregistered* us as a refresh observer, and we 1111 // don't want to mistakenly overwrite mObservingState and fool ourselves 1112 // into thinking we've re-registered when we really haven't! 1113 MOZ_ASSERT(mObservingState == eNotObservingRefresh, 1114 "We've been shutdown, which means we should've been " 1115 "unregistered as a refresh observer"); 1116 return; 1117 } 1118 mObservingState = eRefreshObserving; 1119 1120 // Stop further processing if there are no new notifications of any kind or 1121 // events and document load is processed. 1122 if (mContentInsertions.Count() == 0 && mNotifications.IsEmpty() && 1123 !mFocusEvent && mEvents.IsEmpty() && mTextArray.IsEmpty() && 1124 mHangingChildDocuments.IsEmpty() && 1125 mDocument->HasLoadState(DocAccessible::eCompletelyLoaded) && 1126 mPresShell->RemoveRefreshObserver(this, FlushType::Display)) { 1127 mObservingState = eNotObservingRefresh; 1128 } 1129 } 1130 1131 void NotificationController::EventMap::PutEvent(AccTreeMutationEvent* aEvent) { 1132 EventType type = GetEventType(aEvent); 1133 uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); 1134 MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); 1135 addr |= type; 1136 mTable.InsertOrUpdate(addr, RefPtr{aEvent}); 1137 } 1138 1139 AccTreeMutationEvent* NotificationController::EventMap::GetEvent( 1140 LocalAccessible* aTarget, EventType aType) { 1141 uint64_t addr = reinterpret_cast<uintptr_t>(aTarget); 1142 MOZ_ASSERT((addr & 0x3) == 0, "target is not 4 byte aligned"); 1143 1144 addr |= aType; 1145 return mTable.GetWeak(addr); 1146 } 1147 1148 void NotificationController::EventMap::RemoveEvent( 1149 AccTreeMutationEvent* aEvent) { 1150 EventType type = GetEventType(aEvent); 1151 uint64_t addr = reinterpret_cast<uintptr_t>(aEvent->GetAccessible()); 1152 MOZ_ASSERT((addr & 0x3) == 0, "accessible is not 4 byte aligned"); 1153 addr |= type; 1154 1155 MOZ_ASSERT(mTable.GetWeak(addr) == aEvent, "mTable has the wrong event"); 1156 mTable.Remove(addr); 1157 } 1158 1159 NotificationController::EventMap::EventType 1160 NotificationController::EventMap::GetEventType(AccTreeMutationEvent* aEvent) { 1161 switch (aEvent->GetEventType()) { 1162 case nsIAccessibleEvent::EVENT_SHOW: 1163 return ShowEvent; 1164 case nsIAccessibleEvent::EVENT_HIDE: 1165 return HideEvent; 1166 case nsIAccessibleEvent::EVENT_REORDER: 1167 case nsIAccessibleEvent::EVENT_INNER_REORDER: 1168 return ReorderEvent; 1169 default: 1170 MOZ_ASSERT_UNREACHABLE("event has invalid type"); 1171 return ShowEvent; 1172 } 1173 }