Files
sousa-gecko/dom/base/MutationObservers.cpp
mayankleoboy1 0ed984469b Bug 2000355 - Skip to the lowest observed ancestor on repeated mutations r=smaug
ForEachAncestorObserver walks the inclusive ancestor chain on every mutation,
which is O(depth). To improve performance, nsINode memoizes one walk's start
node and the lowest ancestor found to hold an observer, and a later walk from
the same node starts there.

The memo only picks where a walk starts, never what it returns. It is dropped
when an observer is registered anywhere, or when certain mutations occur.

Testcase goes 8346 -> 2342 samples, with the walk down from 53% to 0.8%.
Speedometer3 is neutral. mach try auto is clean.

Differential Revision: https://phabricator.services.mozilla.com/D314591
2026-08-13 04:55:57 +00:00

269 lines
10 KiB
C++

/* 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 "MutationObservers.h"
#include "PLDHashTable.h"
#include "PseudoStyleType.h"
#include "mozilla/AnimationTarget.h"
#include "mozilla/Assertions.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/EventListenerManager.h"
#include "mozilla/PresShell.h"
#include "mozilla/dom/Animation.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLTemplateElement.h"
#include "mozilla/dom/KeyframeEffect.h"
#include "mozilla/dom/ShadowRoot.h"
#include "nsCOMArray.h"
#include "nsContentUtils.h"
#include "nsDOMMutationObserver.h"
#include "nsGenericHTMLElement.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "nsIMutationObserver.h"
#include "nsINode.h"
#include "nsPIDOMWindow.h"
#include "nsWrapperCacheInlines.h"
#include "nsXULElement.h"
using namespace mozilla;
using namespace mozilla::dom;
#define NOTIFY_PRESSHELL(notify_) \
if (PresShell* presShell = doc->GetObservingPresShell()) { \
notify_(presShell); \
}
#define NOTIFIER(func_, ...) \
[&](nsIMutationObserver* aObserver) { aObserver->func_(__VA_ARGS__); }
template <typename NotifyObserver>
static inline nsINode* ForEachAncestorObserver(nsINode* aNode,
NotifyObserver& aFunc,
uint32_t aCallback) {
// Nodes below the lowest observed ancestor cost a slots load and yield
// nothing, so a previous walk from this node lets us start above them.
nsINode* node = nsINode::ObserverChainSkipTo(aNode);
#ifdef DEBUG
if (node) {
nsINode* debugNode = aNode;
nsINode* debugLast = nullptr;
while (debugNode && debugNode != node) {
if (debugNode->GetMutationObservers()) {
MOZ_ASSERT(false, "skipped nodes must not have observers");
}
debugLast = debugNode;
if (!(debugNode = debugNode->GetParentNode())) {
if (ShadowRoot* shadow = ShadowRoot::FromNode(debugLast)) {
debugNode = shadow->GetHost();
}
}
}
MOZ_ASSERT(debugNode == node,
"cached skip-to node must be a valid ancestor");
}
#endif
bool memoize = !node && aNode->IsInComposedDoc();
if (!node) {
node = aNode;
}
nsINode* last;
do {
mozilla::SafeDoublyLinkedList<nsIMutationObserver>* observers =
node->GetMutationObservers();
if (observers) {
if (memoize && node != aNode) {
nsINode::NoteObserverChain(aNode, node);
}
memoize = false;
for (auto iter = observers->begin(); iter != observers->end(); ++iter) {
if (iter->IsCallbackEnabled(aCallback)) {
aFunc(&*iter);
}
}
}
last = node;
if (!(node = node->GetParentNode())) {
if (ShadowRoot* shadow = ShadowRoot::FromNode(last)) {
node = shadow->GetHost();
}
}
} while (node);
// Nothing observed anything, so a later walk can go straight to the end.
if (memoize && last != aNode) {
nsINode::NoteObserverChain(aNode, last);
}
return last;
}
// Whether to notify to the PresShell about a mutation.
// For removals, the pres shell gets notified first, since it needs to operate
// on the "old" DOM shape.
enum class NotifyPresShell { No, Before, After };
template <NotifyPresShell aNotifyPresShell = NotifyPresShell::After,
typename NotifyObserver>
static inline void Notify(nsINode* aNode, NotifyObserver&& aNotify,
uint32_t aCallback) {
Document* doc = aNode->OwnerDoc();
nsDOMMutationEnterLeave enterLeave(doc);
#ifdef DEBUG
const bool wasConnected = aNode->IsInComposedDoc();
#endif
if constexpr (aNotifyPresShell == NotifyPresShell::Before) {
if (aNode->IsInComposedDoc()) {
NOTIFY_PRESSHELL(aNotify);
}
}
nsINode* last = ForEachAncestorObserver(aNode, aNotify, aCallback);
// For non-removals, the pres shell gets notified last, since it needs to
// operate on the "final" DOM shape.
if constexpr (aNotifyPresShell == NotifyPresShell::After) {
if (last == doc) {
NOTIFY_PRESSHELL(aNotify);
}
}
MOZ_ASSERT((last == doc) == wasConnected,
"If we were connected we should notify all ancestors all the "
"way to the document");
}
#define IMPL_ANIMATION_NOTIFICATION(func_, content_, params_) \
PR_BEGIN_MACRO \
nsDOMMutationEnterLeave enterLeave(doc); \
auto forEach = [&](nsIMutationObserver* aObserver) { \
if (nsCOMPtr<nsIAnimationObserver> obs = do_QueryInterface(aObserver)) { \
obs->func_ params_; \
} \
}; \
ForEachAncestorObserver(content_, forEach, nsIMutationObserver::k##func_); \
PR_END_MACRO
namespace mozilla {
void MutationObservers::NotifyCharacterDataWillChange(
nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
Notify(aContent, NOTIFIER(CharacterDataWillChange, aContent, aInfo),
nsIMutationObserver::kCharacterDataWillChange);
}
void MutationObservers::NotifyCharacterDataChanged(
nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
aContent->OwnerDoc()->Changed();
Notify(aContent, NOTIFIER(CharacterDataChanged, aContent, aInfo),
nsIMutationObserver::kCharacterDataChanged);
}
void MutationObservers::NotifyAttributeWillChange(Element* aElement,
int32_t aNameSpaceID,
nsAtom* aAttribute,
AttrModType aModType) {
Notify(aElement,
NOTIFIER(AttributeWillChange, aElement, aNameSpaceID, aAttribute,
aModType),
nsIMutationObserver::kAttributeWillChange);
}
void MutationObservers::NotifyAttributeChanged(Element* aElement,
int32_t aNameSpaceID,
nsAtom* aAttribute,
AttrModType aModType,
const nsAttrValue* aOldValue) {
aElement->OwnerDoc()->Changed();
Notify(aElement,
NOTIFIER(AttributeChanged, aElement, aNameSpaceID, aAttribute,
aModType, aOldValue),
nsIMutationObserver::kAttributeChanged);
}
void MutationObservers::NotifyAttributeSetToCurrentValue(Element* aElement,
int32_t aNameSpaceID,
nsAtom* aAttribute) {
Notify(
aElement,
NOTIFIER(AttributeSetToCurrentValue, aElement, aNameSpaceID, aAttribute),
nsIMutationObserver::kAttributeSetToCurrentValue);
}
void MutationObservers::NotifyContentAppended(nsIContent* aContainer,
nsIContent* aFirstNewContent,
const ContentAppendInfo& aInfo) {
aContainer->OwnerDoc()->Changed();
Notify(aContainer, NOTIFIER(ContentAppended, aFirstNewContent, aInfo),
nsIMutationObserver::kContentAppended);
}
void MutationObservers::NotifyContentInserted(nsINode* aContainer,
nsIContent* aChild,
const ContentInsertInfo& aInfo) {
MOZ_ASSERT(aContainer->IsContent() || aContainer->IsDocument(),
"container must be an nsIContent or an Document");
aContainer->OwnerDoc()->Changed();
Notify(aContainer, NOTIFIER(ContentInserted, aChild, aInfo),
nsIMutationObserver::kContentInserted);
}
void MutationObservers::NotifyContentWillBeRemoved(
nsINode* aContainer, nsIContent* aChild, const ContentRemoveInfo& aInfo) {
MOZ_ASSERT(aContainer->IsContent() || aContainer->IsDocument(),
"container must be an nsIContent or an Document");
MOZ_ASSERT(aChild->GetParentNode() == aContainer,
"We expect the parent link to be still around at this point");
aContainer->OwnerDoc()->Changed();
Notify<NotifyPresShell::Before>(aContainer,
NOTIFIER(ContentWillBeRemoved, aChild, aInfo),
nsIMutationObserver::kContentWillBeRemoved);
}
} // namespace mozilla
void MutationObservers::NotifyAnimationMutated(
dom::Animation* aAnimation, AnimationMutationType aMutatedType) {
MOZ_ASSERT(aAnimation);
NonOwningAnimationTarget target = aAnimation->GetTargetForAnimation();
if (!target) {
return;
}
// A pseudo element and its parent element use the same owner doc.
Document* doc = target.mElement->OwnerDoc();
if (doc->MayHaveAnimationObservers()) {
// we use the its parent element as the subject in DOM Mutation Observer.
Element* elem = target.mElement;
switch (aMutatedType) {
case AnimationMutationType::Added:
IMPL_ANIMATION_NOTIFICATION(AnimationAdded, elem, (aAnimation));
break;
case AnimationMutationType::Changed:
IMPL_ANIMATION_NOTIFICATION(AnimationChanged, elem, (aAnimation));
break;
case AnimationMutationType::Removed:
IMPL_ANIMATION_NOTIFICATION(AnimationRemoved, elem, (aAnimation));
break;
default:
MOZ_ASSERT_UNREACHABLE("unexpected mutation type");
}
}
}
void MutationObservers::NotifyAnimationAdded(dom::Animation* aAnimation) {
NotifyAnimationMutated(aAnimation, AnimationMutationType::Added);
}
void MutationObservers::NotifyAnimationChanged(dom::Animation* aAnimation) {
NotifyAnimationMutated(aAnimation, AnimationMutationType::Changed);
}
void MutationObservers::NotifyAnimationRemoved(dom::Animation* aAnimation) {
NotifyAnimationMutated(aAnimation, AnimationMutationType::Removed);
}