Bug 1188640 - Add ChromeOnly MutationObserver.mergeAttributeRecords to speed up devtools, r=bz,bgrins

This commit is contained in:
Olli Pettay 2015-07-30 00:23:47 +03:00
parent 09e16890c4
commit c4161c2908
5 changed files with 211 additions and 27 deletions

View File

@ -688,7 +688,11 @@ nsDOMMutationObserver::TakeRecords(
for (uint32_t i = 0; i < mPendingMutationCount; ++i) {
nsRefPtr<nsDOMMutationRecord> next;
current->mNext.swap(next);
*aRetVal.AppendElement() = current.forget();
if (!mMergeAttributeRecords ||
!MergeableAttributeRecord(aRetVal.SafeLastElement(nullptr),
current)) {
*aRetVal.AppendElement() = current.forget();
}
current.swap(next);
}
ClearPendingRecords();
@ -745,6 +749,21 @@ nsDOMMutationObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
return observer.forget();
}
bool
nsDOMMutationObserver::MergeableAttributeRecord(nsDOMMutationRecord* aOldRecord,
nsDOMMutationRecord* aRecord)
{
MOZ_ASSERT(mMergeAttributeRecords);
return
aOldRecord &&
aOldRecord->mType == nsGkAtoms::attributes &&
aOldRecord->mType == aRecord->mType &&
aOldRecord->mTarget == aRecord->mTarget &&
aOldRecord->mAttrName == aRecord->mAttrName &&
aOldRecord->mAttrNamespace.Equals(aRecord->mAttrNamespace);
}
void
nsDOMMutationObserver::HandleMutation()
{
@ -776,7 +795,12 @@ nsDOMMutationObserver::HandleMutation()
for (uint32_t i = 0; i < mPendingMutationCount; ++i) {
nsRefPtr<nsDOMMutationRecord> next;
current->mNext.swap(next);
*mutations.AppendElement(mozilla::fallible) = current;
if (!mMergeAttributeRecords ||
!MergeableAttributeRecord(mutations.Length() ?
mutations.LastElement().get() : nullptr,
current)) {
*mutations.AppendElement(mozilla::fallible) = current;
}
current.swap(next);
}
}

View File

@ -456,7 +456,8 @@ public:
mozilla::dom::MutationCallback& aCb,
bool aChrome)
: mOwner(aOwner), mLastPendingMutation(nullptr), mPendingMutationCount(0),
mCallback(&aCb), mWaitingForRun(false), mIsChrome(aChrome), mId(++sCount)
mCallback(&aCb), mWaitingForRun(false), mIsChrome(aChrome),
mMergeAttributeRecords(false), mId(++sCount)
{
}
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
@ -498,6 +499,22 @@ public:
mozilla::dom::MutationCallback* MutationCallback() { return mCallback; }
bool MergeAttributeRecords()
{
return mMergeAttributeRecords;
}
void SetMergeAttributeRecords(bool aVal)
{
mMergeAttributeRecords = aVal;
}
// If both records are for 'attributes' type and for the same target and
// attribute name and namespace are the same, we can skip the newer record.
// aOldRecord->mPrevValue holds the original value, if observed.
bool MergeableAttributeRecord(nsDOMMutationRecord* aOldRecord,
nsDOMMutationRecord* aRecord);
void AppendMutationRecord(already_AddRefed<nsDOMMutationRecord> aRecord)
{
nsRefPtr<nsDOMMutationRecord> record = aRecord;
@ -585,6 +602,7 @@ protected:
bool mWaitingForRun;
bool mIsChrome;
bool mMergeAttributeRecords;
uint64_t mId;

View File

@ -743,7 +743,7 @@ function testStyleRemoveProperty2() {
observer.disconnect();
m = null;
div.removeAttribute("data-test");
then();
then(testAttributeRecordMerging1);
});
m.observe(div, { attributes: true });
is(div.getAttribute("style"), null, "style attribute before unsuccessful removeProperty");
@ -751,6 +751,167 @@ function testStyleRemoveProperty2() {
div.setAttribute("data-test", "a");
}
function testAttributeRecordMerging1() {
ok(true, "testAttributeRecordMerging1");
var m = new M(function(records, observer) {
is(records.length, 2);
is(records[0].type, "attributes");
is(records[0].target, div);
is(records[0].attributeName, "foo");
is(records[0].attributeNamespace, null);
is(records[0].oldValue, null);
is(records[1].type, "attributes");
is(records[1].target, div.firstChild);
is(records[1].attributeName, "foo");
is(records[1].attributeNamespace, null);
is(records[1].oldValue, null);
observer.disconnect();
div.innerHTML = "";
div.removeAttribute("foo");
then(testAttributeRecordMerging2);
});
m.observe(div, {
attributes: true,
subtree: true
});
SpecialPowers.wrap(m).mergeAttributeRecords = true;
div.setAttribute("foo", "bar_1");
div.setAttribute("foo", "bar_2");
div.innerHTML = "<div></div>";
div.firstChild.setAttribute("foo", "bar_1");
div.firstChild.setAttribute("foo", "bar_2");
}
function testAttributeRecordMerging2() {
ok(true, "testAttributeRecordMerging2");
var m = new M(function(records, observer) {
is(records.length, 2);
is(records[0].type, "attributes");
is(records[0].target, div);
is(records[0].attributeName, "foo");
is(records[0].attributeNamespace, null);
is(records[0].oldValue, "initial");
is(records[1].type, "attributes");
is(records[1].target, div.firstChild);
is(records[1].attributeName, "foo");
is(records[1].attributeNamespace, null);
is(records[1].oldValue, "initial");
observer.disconnect();
div.innerHTML = "";
div.removeAttribute("foo");
then(testAttributeRecordMerging3);
});
div.setAttribute("foo", "initial");
div.innerHTML = "<div></div>";
div.firstChild.setAttribute("foo", "initial");
m.observe(div, {
attributes: true,
subtree: true,
attributeOldValue: true
});
SpecialPowers.wrap(m).mergeAttributeRecords = true;
div.setAttribute("foo", "bar_1");
div.setAttribute("foo", "bar_2");
div.firstChild.setAttribute("foo", "bar_1");
div.firstChild.setAttribute("foo", "bar_2");
}
function testAttributeRecordMerging3() {
ok(true, "testAttributeRecordMerging3");
var m = new M(function(records, observer) {
is(records.length, 4);
is(records[0].type, "attributes");
is(records[0].target, div);
is(records[0].attributeName, "foo");
is(records[0].attributeNamespace, null);
is(records[0].oldValue, "initial");
is(records[1].type, "attributes");
is(records[1].target, div.firstChild);
is(records[1].attributeName, "foo");
is(records[1].attributeNamespace, null);
is(records[1].oldValue, "initial");
is(records[2].type, "attributes");
is(records[2].target, div);
is(records[2].attributeName, "foo");
is(records[2].attributeNamespace, null);
is(records[2].oldValue, "bar_1");
is(records[3].type, "attributes");
is(records[3].target, div.firstChild);
is(records[3].attributeName, "foo");
is(records[3].attributeNamespace, null);
is(records[3].oldValue, "bar_1");
observer.disconnect();
div.innerHTML = "";
div.removeAttribute("foo");
then(testAttributeRecordMerging4);
});
div.setAttribute("foo", "initial");
div.innerHTML = "<div></div>";
div.firstChild.setAttribute("foo", "initial");
m.observe(div, {
attributes: true,
subtree: true,
attributeOldValue: true
});
SpecialPowers.wrap(m).mergeAttributeRecords = true;
// No merging should happen.
div.setAttribute("foo", "bar_1");
div.firstChild.setAttribute("foo", "bar_1");
div.setAttribute("foo", "bar_2");
div.firstChild.setAttribute("foo", "bar_2");
}
function testAttributeRecordMerging4() {
ok(true, "testAttributeRecordMerging4");
var m = new M(function(records, observer) {
});
div.setAttribute("foo", "initial");
div.innerHTML = "<div></div>";
div.firstChild.setAttribute("foo", "initial");
m.observe(div, {
attributes: true,
subtree: true,
attributeOldValue: true
});
SpecialPowers.wrap(m).mergeAttributeRecords = true;
div.setAttribute("foo", "bar_1");
div.setAttribute("foo", "bar_2");
div.firstChild.setAttribute("foo", "bar_1");
div.firstChild.setAttribute("foo", "bar_2");
var records = m.takeRecords();
is(records.length, 2);
is(records[0].type, "attributes");
is(records[0].target, div);
is(records[0].attributeName, "foo");
is(records[0].attributeNamespace, null);
is(records[0].oldValue, "initial");
is(records[1].type, "attributes");
is(records[1].target, div.firstChild);
is(records[1].attributeName, "foo");
is(records[1].attributeNamespace, null);
is(records[1].oldValue, "initial");
m.disconnect();
div.innerHTML = "";
div.removeAttribute("foo");
then();
}
SimpleTest.waitForExplicitFinish();
</script>

View File

@ -47,6 +47,8 @@ interface MutationObserver {
sequence<MutationObservingInfo?> getObservingInfo();
[ChromeOnly]
readonly attribute MutationCallback mutationCallback;
[ChromeOnly]
attribute boolean mergeAttributeRecords;
};
callback MutationCallback = void (sequence<MutationRecord> mutations, MutationObserver observer);

View File

@ -1471,6 +1471,7 @@ var WalkerActor = protocol.ActorClass({
// Create the observer on the node's actor. The node will make sure
// the observer is cleaned up when the actor is released.
actor.observer = new actor.rawNode.defaultView.MutationObserver(this.onMutations);
actor.observer.mergeAttributeRecords = true;
actor.observer.observe(node, {
attributes: true,
characterData: true,
@ -2736,29 +2737,7 @@ var WalkerActor = protocol.ActorClass({
this._orphaned = new Set();
}
// Clear out any duplicate attribute mutations before sending them over
// the protocol. Keep only the most recent change for each attribute.
let targetMap = {};
let filtered = pending.reverse().filter(mutation => {
if (mutation.type === "attributes") {
if (!targetMap[mutation.target]) {
targetMap[mutation.target] = {};
}
let attributesForTarget = targetMap[mutation.target];
if (attributesForTarget[mutation.attributeName]) {
// Since the array was reversed, if we've seen this attribute already
// then this one is a duplicate and can be skipped.
return false;
}
attributesForTarget[mutation.attributeName] = true;
}
return true;
}).reverse();
return filtered;
return pending;
}, {
request: {
cleanup: Option(0)