Bug 1694548 - Move devtools breakpoints out of mutation events. r=smaug,jdescottes

Use internal events which get fired for CSSOM changes, and in Shadow
DOM. This will also allow us to remove mutation events in the future.

Depends on D106273

Differential Revision: https://phabricator.services.mozilla.com/D106274
This commit is contained in:
Emilio Cobos Álvarez 2021-03-02 18:57:31 +00:00
Родитель 156a1feb88
Коммит 8508ab62bb
8 изменённых файлов: 136 добавлений и 51 удалений

Просмотреть файл

@ -15,12 +15,16 @@ Services.scriptloader.loadSubScript(
const DMB_TEST_URL =
"http://example.com/browser/devtools/client/debugger/test/mochitest/examples/doc-dom-mutation.html";
add_task(async function() {
// Enable features
async function enableMutationBreakpoints() {
await pushPref("devtools.debugger.features.dom-mutation-breakpoints", true);
await pushPref("devtools.markup.mutationBreakpoints.enabled", true);
await pushPref("devtools.debugger.dom-mutation-breakpoints-visible", true);
}
add_task(async function() {
// Enable features
await enableMutationBreakpoints();
info("Switches over to the inspector pane");
const { inspector, toolbox } = await openInspectorForURL(DMB_TEST_URL);
@ -66,6 +70,13 @@ add_task(async function() {
await waitForPaused(dbg);
await resume(dbg);
info("Changing style to trigger debugger pause");
SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() {
content.document.querySelector("#style-attribute").click();
});
await waitForPaused(dbg);
await resume(dbg);
info("Changing subtree to trigger debugger pause");
SpecialPowers.spawn(gBrowser.selectedBrowser, [], function() {
content.document.querySelector("#subtree").click();

Просмотреть файл

@ -10,6 +10,7 @@
<body>
<button title="Hello" id="attribute" onclick="changeAttribute()">Click me!</button>
<button id="style-attribute" onclick="changeStyleAttribute()">Click me!</button>
<button id="subtree" onclick="changeSubtree()">Click me!</button>
<button id="blackbox" onclick="changeAttribute();&#xA;debugger;&#xA;//# sourceURL=click.js">Click me!</button>
<script src="dom-mutation.js"></script>

Просмотреть файл

@ -3,6 +3,10 @@ function changeAttribute() {
document.body.setAttribute("title", title);
}
function changeStyleAttribute() {
document.body.style.color = "blue";
}
function changeSubtree() {
document.body.appendChild(document.createElement("div"));
}

Просмотреть файл

@ -231,8 +231,8 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
// list contains orphaned nodes that were so retained.
this._retainedOrphans = new Set();
this.onNodeInserted = this.onNodeInserted.bind(this);
this.onNodeInserted[EXCLUDED_LISTENER] = true;
this.onSubtreeModified = this.onSubtreeModified.bind(this);
this.onSubtreeModified[EXCLUDED_LISTENER] = true;
this.onNodeRemoved = this.onNodeRemoved.bind(this);
this.onNodeRemoved[EXCLUDED_LISTENER] = true;
this.onAttributeModified = this.onAttributeModified.bind(this);
@ -2076,65 +2076,58 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
_updateDocumentMutationListeners(rawDoc) {
const docMutationBreakpoints = this._mutationBreakpointsForDoc(rawDoc);
if (!docMutationBreakpoints) {
rawDoc.devToolsWatchingDOMMutations = false;
return;
}
const origFlag = rawDoc.devToolsWatchingDOMMutations;
rawDoc.devToolsWatchingDOMMutations = true;
const anyBreakpoint =
docMutationBreakpoints.counts.subtree > 0 ||
docMutationBreakpoints.counts.removal > 0 ||
docMutationBreakpoints.counts.attribute > 0;
rawDoc.devToolsWatchingDOMMutations = anyBreakpoint;
if (docMutationBreakpoints.counts.subtree > 0) {
eventListenerService.addSystemEventListener(
rawDoc,
"DOMNodeInserted",
this.onNodeInserted,
this.chromeEventHandler.addEventListener(
"devtoolschildinserted",
this.onSubtreeModified,
true /* capture */
);
} else {
eventListenerService.removeSystemEventListener(
rawDoc,
"DOMNodeInserted",
this.onNodeInserted,
this.chromeEventHandler.removeEventListener(
"devtoolschildinserted",
this.onSubtreeModified,
true /* capture */
);
}
if (
docMutationBreakpoints.counts.subtree > 0 ||
docMutationBreakpoints.counts.removal > 0 ||
docMutationBreakpoints.counts.attribute > 0
) {
eventListenerService.addSystemEventListener(
rawDoc,
"DOMNodeRemoved",
if (anyBreakpoint) {
this.chromeEventHandler.addEventListener(
"devtoolschildremoved",
this.onNodeRemoved,
true /* capture */
);
} else {
eventListenerService.removeSystemEventListener(
rawDoc,
"DOMNodeRemoved",
this.chromeEventHandler.removeEventListener(
"devtoolschildremoved",
this.onNodeRemoved,
true /* capture */
);
}
if (docMutationBreakpoints.counts.attribute > 0) {
eventListenerService.addSystemEventListener(
rawDoc,
"DOMAttrModified",
this.chromeEventHandler.addEventListener(
"devtoolsattrmodified",
this.onAttributeModified,
true /* capture */
);
} else {
eventListenerService.removeSystemEventListener(
rawDoc,
"DOMAttrModified",
this.chromeEventHandler.removeEventListener(
"devtoolsattrmodified",
this.onAttributeModified,
true /* capture */
);
}
rawDoc.devToolsWatchingDOMMutations = origFlag;
},
_breakOnMutation: function(mutationType, targetNode, ancestorNode, action) {
@ -2172,20 +2165,15 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
);
},
onNodeInserted: function(evt) {
this.onSubtreeModified(evt, "add");
},
onNodeRemoved: function(evt) {
const mutationBpInfo = this._breakpointInfoForNode(evt.target);
const hasNodeRemovalEvent = mutationBpInfo?.removal;
this._clearMutationBreakpointsFromSubtree(evt.target);
if (hasNodeRemovalEvent) {
this._breakOnMutation("nodeRemoved", evt.target);
} else {
this.onSubtreeModified(evt, "remove");
this.onSubtreeModified(evt);
}
},
@ -2196,7 +2184,8 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
}
},
onSubtreeModified: function(evt, action) {
onSubtreeModified: function(evt) {
const action = evt.type === "devtoolschildinserted" ? "add" : "remove";
let node = evt.target;
while ((node = node.parentNode) !== null) {
const mutationBpInfo = this._breakpointInfoForNode(node);

Просмотреть файл

@ -9567,8 +9567,7 @@ nsINode* Document::AdoptNode(nsINode& aAdoptedNode, ErrorResult& rv) {
// Scope firing mutation events so that we don't carry any state that
// might be stale
{
nsINode* parent = adoptedNode->GetParentNode();
if (parent) {
if (nsINode* parent = adoptedNode->GetParentNode()) {
nsContentUtils::MaybeFireNodeRemoved(adoptedNode, parent);
}
}
@ -10830,9 +10829,12 @@ bool Document::CanSavePresentation(nsIRequest* aNewRequest,
void Document::Destroy() {
// The ContentViewer wants to release the document now. So, tell our content
// to drop any references to the document so that it can be destroyed.
if (mIsGoingAway) return;
if (mIsGoingAway) {
return;
}
ReportDocumentUseCounters();
SetDevToolsWatchingDOMMutations(false);
mIsGoingAway = true;
@ -13310,6 +13312,80 @@ already_AddRefed<nsINode> Document::GetTooltipNode() {
return nullptr;
}
namespace {
class DevToolsMutationObserver final : public nsStubMutationObserver {
NS_DECL_ISUPPORTS
NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
// We handle this in nsContentUtils::MaybeFireNodeRemoved, since devtools
// relies on the event firing _before_ the removal happens.
// NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
// NOTE(emilio, bug 1694627): DevTools doesn't seem to deal with character
// data changes right now (maybe intentionally?).
// NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
DevToolsMutationObserver() = default;
private:
void FireEvent(nsINode* aTarget, const nsAString& aType);
~DevToolsMutationObserver() = default;
};
NS_IMPL_ISUPPORTS(DevToolsMutationObserver, nsIMutationObserver)
void DevToolsMutationObserver::FireEvent(nsINode* aTarget,
const nsAString& aType) {
if (aTarget->ChromeOnlyAccess()) {
return;
}
(new AsyncEventDispatcher(aTarget, aType, CanBubble::eNo,
ChromeOnlyDispatch::eYes, Composed::eYes))
->RunDOMEventWhenSafe();
}
void DevToolsMutationObserver::AttributeChanged(Element* aElement,
int32_t aNamespaceID,
nsAtom* aAttribute,
int32_t aModType,
const nsAttrValue* aOldValue) {
FireEvent(aElement, u"devtoolsattrmodified"_ns);
}
void DevToolsMutationObserver::ContentAppended(nsIContent* aFirstNewContent) {
for (nsIContent* c = aFirstNewContent; c; c = c->GetNextSibling()) {
ContentInserted(c);
}
}
void DevToolsMutationObserver::ContentInserted(nsIContent* aChild) {
FireEvent(aChild, u"devtoolschildinserted"_ns);
}
static StaticRefPtr<DevToolsMutationObserver> sDevToolsMutationObserver;
} // namespace
void Document::SetDevToolsWatchingDOMMutations(bool aValue) {
if (mDevToolsWatchingDOMMutations == aValue || mIsGoingAway) {
return;
}
mDevToolsWatchingDOMMutations = aValue;
if (aValue) {
if (MOZ_UNLIKELY(!sDevToolsMutationObserver)) {
sDevToolsMutationObserver = new DevToolsMutationObserver();
ClearOnShutdown(&sDevToolsMutationObserver);
}
AddMutationObserver(sDevToolsMutationObserver);
} else if (sDevToolsMutationObserver) {
RemoveMutationObserver(sDevToolsMutationObserver);
}
}
void Document::MaybeWarnAboutZoom() {
if (mHasWarnedAboutZoom) {
return;

Просмотреть файл

@ -3549,9 +3549,7 @@ class Document : public nsINode,
bool DevToolsWatchingDOMMutations() const {
return mDevToolsWatchingDOMMutations;
}
void SetDevToolsWatchingDOMMutations(bool aValue) {
mDevToolsWatchingDOMMutations = aValue;
}
void SetDevToolsWatchingDOMMutations(bool aValue);
void MaybeWarnAboutZoom();

Просмотреть файл

@ -4617,8 +4617,7 @@ void nsContentUtils::MaybeFireNodeRemoved(nsINode* aChild, nsINode* aParent) {
// that is a know case when we'd normally fire a mutation event, but can't
// make that safe and so we suppress it at this time. Ideally this should
// go away eventually.
if (!(aChild->IsContent() &&
aChild->AsContent()->IsInNativeAnonymousSubtree()) &&
if (!aChild->IsInNativeAnonymousSubtree() &&
!sDOMNodeRemovedSuppressCount) {
NS_ERROR("Want to fire DOMNodeRemoved event, but it's not safe");
WarnScriptWasIgnored(aChild->OwnerDoc());
@ -4626,6 +4625,15 @@ void nsContentUtils::MaybeFireNodeRemoved(nsINode* aChild, nsINode* aParent) {
return;
}
{
Document* doc = aParent->OwnerDoc();
if (MOZ_UNLIKELY(doc->DevToolsWatchingDOMMutations()) &&
aChild->IsInComposedDoc() && !aChild->ChromeOnlyAccess()) {
DispatchChromeEvent(doc, aChild, u"devtoolschildremoved"_ns,
CanBubble::eNo, Cancelable::eNo);
}
}
if (HasMutationListeners(aChild, NS_EVENT_BITS_MUTATION_NODEREMOVED,
aParent)) {
InternalMutationEvent mutation(true, eLegacyNodeRemoved);

Просмотреть файл

@ -287,9 +287,7 @@ void EventListenerManager::AddEventListenerInternal(
mMayHaveMutationListeners = true;
// Go from our target to the nearest enclosing DOM window.
if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) {
nsCOMPtr<Document> doc = window->GetExtantDoc();
if (doc &&
!(aFlags.mInSystemGroup && doc->DevToolsWatchingDOMMutations())) {
if (Document* doc = window->GetExtantDoc()) {
doc->WarnOnceAbout(DeprecatedOperations::eMutationEvent);
}
// If aEventMessage is eLegacySubtreeModified, we need to listen all