[blink] Early exit on no-op changes to style and class attributes.
We observed real-world sites (see bug) making repetitive no-op changes to the style or class attribute of an element, triggering a style recalc every time. This CL adds a feature to exit early when that happens. We'll run an experiment to assess the impact on CPU usage. Most of the complexity in this CL comes from the fact that the early exit cannot happen when the class attribute is set as part of moving an element between documents with different "quirks mode". In this case, the attribute's value must be re-evaluated using the new "quirks mode". Bug: 1332843 Change-Id: Ifa8787be4e4df4bc6f72a67ec52dc84cb0c4b210 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3689592 Reviewed-by: David Baron <dbaron@chromium.org> Commit-Queue: Francois Pierre Doray <fdoray@chromium.org> Reviewed-by: Rune Lillesveen <futhark@chromium.org> Cr-Commit-Position: refs/heads/main@{#1012605}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
1acfb2414f
commit
bed5f197d6
third_party/blink
3
third_party/blink/common/features.cc
vendored
3
third_party/blink/common/features.cc
vendored
@ -1532,5 +1532,8 @@ const base::Feature kWebSQLNonSecureContextAccess{
|
||||
const base::Feature kFileSystemUrlNavigation{"FileSystemUrlNavigation",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
const base::Feature kEarlyExitOnNoopClassOrStyleChange{
|
||||
"EarlyExitOnNoopClassOrStyleChange", base::FEATURE_DISABLED_BY_DEFAULT};
|
||||
|
||||
} // namespace features
|
||||
} // namespace blink
|
||||
|
5
third_party/blink/public/common/features.h
vendored
5
third_party/blink/public/common/features.h
vendored
@ -775,6 +775,11 @@ BLINK_COMMON_EXPORT extern const base::Feature kWebSQLNonSecureContextAccess;
|
||||
// TODO(https://crbug.com/1332598): Remove this feature.
|
||||
BLINK_COMMON_EXPORT extern const base::Feature kFileSystemUrlNavigation;
|
||||
|
||||
// Early exit when the style or class attribute of an element is set to the same
|
||||
// value as before.
|
||||
BLINK_COMMON_EXPORT extern const base::Feature
|
||||
kEarlyExitOnNoopClassOrStyleChange;
|
||||
|
||||
} // namespace features
|
||||
} // namespace blink
|
||||
|
||||
|
123
third_party/blink/renderer/core/dom/element.cc
vendored
123
third_party/blink/renderer/core/dom/element.cc
vendored
@ -32,7 +32,9 @@
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "base/feature_list.h"
|
||||
#include "cc/input/snap_selection_strategy.h"
|
||||
#include "third_party/blink/public/common/features.h"
|
||||
#include "third_party/blink/public/common/privacy_budget/identifiability_metric_builder.h"
|
||||
#include "third_party/blink/public/common/privacy_budget/identifiability_study_settings.h"
|
||||
#include "third_party/blink/public/mojom/scroll/scroll_into_view_params.mojom-blink.h"
|
||||
@ -204,6 +206,14 @@ enum class ClassStringContent { kEmpty, kWhiteSpaceOnly, kHasClasses };
|
||||
|
||||
namespace {
|
||||
|
||||
bool EarlyExitOnNoopClassOrStyleChange() {
|
||||
static const bool is_enabled = base::FeatureList::IsEnabled(
|
||||
features::kEarlyExitOnNoopClassOrStyleChange);
|
||||
DCHECK_EQ(is_enabled, base::FeatureList::IsEnabled(
|
||||
features::kEarlyExitOnNoopClassOrStyleChange));
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
class DisplayLockStyleScope {
|
||||
STACK_ALLOCATED();
|
||||
|
||||
@ -802,7 +812,7 @@ Attr* Element::DetachAttribute(wtf_size_t index) {
|
||||
} else {
|
||||
attr_node = MakeGarbageCollected<Attr>(GetDocument(), attribute.GetName(),
|
||||
attribute.Value());
|
||||
RemoveAttributeInternal(index, kNotInSynchronizationOfLazyAttribute);
|
||||
RemoveAttributeInternal(index, AttributeModificationReason::kDirectly);
|
||||
}
|
||||
return attr_node;
|
||||
}
|
||||
@ -814,18 +824,15 @@ void Element::DetachAttrNodeAtIndex(Attr* attr, wtf_size_t index) {
|
||||
const Attribute& attribute = GetElementData()->Attributes().at(index);
|
||||
DCHECK(attribute.GetName() == attr->GetQualifiedName());
|
||||
DetachAttrNodeFromElementWithValue(attr, attribute.Value());
|
||||
RemoveAttributeInternal(index, kNotInSynchronizationOfLazyAttribute);
|
||||
RemoveAttributeInternal(index, AttributeModificationReason::kDirectly);
|
||||
}
|
||||
|
||||
void Element::removeAttribute(const QualifiedName& name) {
|
||||
if (!GetElementData())
|
||||
return;
|
||||
|
||||
wtf_size_t index = GetElementData()->Attributes().FindIndex(name);
|
||||
wtf_size_t index = FindAttributeIndex(name);
|
||||
if (index == kNotFound)
|
||||
return;
|
||||
|
||||
RemoveAttributeInternal(index, kNotInSynchronizationOfLazyAttribute);
|
||||
RemoveAttributeInternal(index, AttributeModificationReason::kDirectly);
|
||||
}
|
||||
|
||||
void Element::SetBooleanAttribute(const QualifiedName& name, bool value) {
|
||||
@ -2342,6 +2349,11 @@ void Element::AttributeChanged(const AttributeModificationParams& params) {
|
||||
GetDocument().GetStyleEngine().IdChangedForElement(old_id, new_id, *this);
|
||||
}
|
||||
} else if (name == html_names::kClassAttr) {
|
||||
if (params.old_value == params.new_value &&
|
||||
params.reason != AttributeModificationReason::kByMoveToNewDocument &&
|
||||
EarlyExitOnNoopClassOrStyleChange()) {
|
||||
return;
|
||||
}
|
||||
ClassAttributeChanged(params.new_value);
|
||||
UpdateClassList(params.old_value, params.new_value);
|
||||
} else if (name == html_names::kNameAttr) {
|
||||
@ -2356,6 +2368,10 @@ void Element::AttributeChanged(const AttributeModificationParams& params) {
|
||||
SynchronizeContentAttributeAndElementReference(name);
|
||||
} else if (IsStyledElement()) {
|
||||
if (name == html_names::kStyleAttr) {
|
||||
if (params.old_value == params.new_value &&
|
||||
EarlyExitOnNoopClassOrStyleChange()) {
|
||||
return;
|
||||
}
|
||||
StyleAttributeChanged(params.new_value, params.reason);
|
||||
} else if (IsPresentationAttribute(name)) {
|
||||
GetElementData()->SetPresentationAttributeStyleIsDirty(true);
|
||||
@ -4980,9 +4996,8 @@ void Element::setAttributeNS(const AtomicString& namespace_uri,
|
||||
setAttribute(parsed_name, value);
|
||||
}
|
||||
|
||||
void Element::RemoveAttributeInternal(
|
||||
wtf_size_t index,
|
||||
SynchronizationOfLazyAttribute in_synchronization_of_lazy_attribute) {
|
||||
void Element::RemoveAttributeInternal(wtf_size_t index,
|
||||
AttributeModificationReason reason) {
|
||||
MutableAttributeCollection attributes =
|
||||
EnsureUniqueElementData().Attributes();
|
||||
SECURITY_DCHECK(index < attributes.size());
|
||||
@ -4990,7 +5005,8 @@ void Element::RemoveAttributeInternal(
|
||||
QualifiedName name = attributes[index].GetName();
|
||||
AtomicString value_being_removed = attributes[index].Value();
|
||||
|
||||
if (!in_synchronization_of_lazy_attribute) {
|
||||
if (reason !=
|
||||
AttributeModificationReason::kBySynchronizationOfLazyAttribute) {
|
||||
if (!value_being_removed.IsNull()) {
|
||||
WillModifyAttribute(name, value_being_removed, g_null_atom);
|
||||
} else if (GetCustomElementState() == CustomElementState::kCustom) {
|
||||
@ -5005,18 +5021,17 @@ void Element::RemoveAttributeInternal(
|
||||
|
||||
attributes.Remove(index);
|
||||
|
||||
if (!in_synchronization_of_lazy_attribute)
|
||||
if (reason != AttributeModificationReason::kBySynchronizationOfLazyAttribute)
|
||||
DidRemoveAttribute(name, value_being_removed);
|
||||
}
|
||||
|
||||
void Element::AppendAttributeInternal(
|
||||
const QualifiedName& name,
|
||||
const AtomicString& value,
|
||||
SynchronizationOfLazyAttribute in_synchronization_of_lazy_attribute) {
|
||||
if (!in_synchronization_of_lazy_attribute)
|
||||
void Element::AppendAttributeInternal(const QualifiedName& name,
|
||||
const AtomicString& value,
|
||||
AttributeModificationReason reason) {
|
||||
if (reason != AttributeModificationReason::kBySynchronizationOfLazyAttribute)
|
||||
WillModifyAttribute(name, g_null_atom, value);
|
||||
EnsureUniqueElementData().Attributes().Append(name, value);
|
||||
if (!in_synchronization_of_lazy_attribute)
|
||||
if (reason != AttributeModificationReason::kBySynchronizationOfLazyAttribute)
|
||||
DidAddAttribute(name, value);
|
||||
}
|
||||
|
||||
@ -7235,11 +7250,12 @@ void Element::DidAddAttribute(const QualifiedName& name,
|
||||
|
||||
void Element::DidModifyAttribute(const QualifiedName& name,
|
||||
const AtomicString& old_value,
|
||||
const AtomicString& new_value) {
|
||||
const AtomicString& new_value,
|
||||
AttributeModificationReason reason) {
|
||||
if (name == html_names::kIdAttr)
|
||||
UpdateId(old_value, new_value);
|
||||
AttributeChanged(AttributeModificationParams(
|
||||
name, old_value, new_value, AttributeModificationReason::kDirectly));
|
||||
AttributeChanged(
|
||||
AttributeModificationParams(name, old_value, new_value, reason));
|
||||
probe::DidModifyDOMAttr(this, name, new_value);
|
||||
// Do not dispatch a DOMSubtreeModified event here; see bug 81141.
|
||||
}
|
||||
@ -7292,10 +7308,17 @@ void Element::DidMoveToNewDocument(Document& old_document) {
|
||||
// element should point to the shareable one.
|
||||
EnsureUniqueElementData();
|
||||
|
||||
if (HasID())
|
||||
SetIdAttribute(GetIdAttribute());
|
||||
if (HasClass())
|
||||
setAttribute(html_names::kClassAttr, GetClassAttribute());
|
||||
if (const AtomicString& idAttr = GetIdAttribute())
|
||||
SetIdAttribute(idAttr);
|
||||
if (const AtomicString& classAttr = GetClassAttribute()) {
|
||||
// Going through setAttribute() to synchronize the attribute is only
|
||||
// required when setting the "style" attribute (this sets the "class"
|
||||
// attribute) or for an SVG element (in which case `GetClassAttribute`
|
||||
// above would already have synchronized).
|
||||
SetAttributeInternal(FindAttributeIndex(html_names::kClassAttr),
|
||||
html_names::kClassAttr, classAttr,
|
||||
AttributeModificationReason::kByMoveToNewDocument);
|
||||
}
|
||||
}
|
||||
// TODO(tkent): Even if Documents' modes are same, keeping
|
||||
// ShareableElementData owned by old_document isn't right.
|
||||
@ -8196,20 +8219,14 @@ std::pair<wtf_size_t, const QualifiedName> Element::LookupAttributeQNameHinted(
|
||||
void Element::setAttribute(const QualifiedName& name,
|
||||
const AtomicString& value) {
|
||||
SynchronizeAttribute(name);
|
||||
wtf_size_t index = GetElementData()
|
||||
? GetElementData()->Attributes().FindIndex(name)
|
||||
: kNotFound;
|
||||
SetAttributeInternal(index, name, value,
|
||||
kNotInSynchronizationOfLazyAttribute);
|
||||
SetAttributeInternal(FindAttributeIndex(name), name, value,
|
||||
AttributeModificationReason::kDirectly);
|
||||
}
|
||||
|
||||
void Element::setAttribute(const QualifiedName& name,
|
||||
const AtomicString& value,
|
||||
ExceptionState& exception_state) {
|
||||
SynchronizeAttribute(name);
|
||||
wtf_size_t index = GetElementData()
|
||||
? GetElementData()->Attributes().FindIndex(name)
|
||||
: kNotFound;
|
||||
|
||||
AtomicString trusted_value(
|
||||
TrustedTypesCheckFor(ExpectedTrustedTypeForAttribute(name), value,
|
||||
@ -8217,16 +8234,15 @@ void Element::setAttribute(const QualifiedName& name,
|
||||
if (exception_state.HadException())
|
||||
return;
|
||||
|
||||
SetAttributeInternal(index, name, trusted_value,
|
||||
kNotInSynchronizationOfLazyAttribute);
|
||||
SetAttributeInternal(FindAttributeIndex(name), name, trusted_value,
|
||||
AttributeModificationReason::kDirectly);
|
||||
}
|
||||
|
||||
void Element::SetSynchronizedLazyAttribute(const QualifiedName& name,
|
||||
const AtomicString& value) {
|
||||
wtf_size_t index = GetElementData()
|
||||
? GetElementData()->Attributes().FindIndex(name)
|
||||
: kNotFound;
|
||||
SetAttributeInternal(index, name, value, kInSynchronizationOfLazyAttribute);
|
||||
SetAttributeInternal(
|
||||
FindAttributeIndex(name), name, value,
|
||||
AttributeModificationReason::kBySynchronizationOfLazyAttribute);
|
||||
}
|
||||
|
||||
void Element::SetAttributeHinted(AtomicString local_name,
|
||||
@ -8253,7 +8269,7 @@ void Element::SetAttributeHinted(AtomicString local_name,
|
||||
return;
|
||||
|
||||
SetAttributeInternal(index, q_name, trusted_value,
|
||||
kNotInSynchronizationOfLazyAttribute);
|
||||
AttributeModificationReason::kDirectly);
|
||||
}
|
||||
|
||||
void Element::SetAttributeHinted(AtomicString local_name,
|
||||
@ -8278,23 +8294,28 @@ void Element::SetAttributeHinted(AtomicString local_name,
|
||||
if (exception_state.HadException())
|
||||
return;
|
||||
SetAttributeInternal(index, q_name, value,
|
||||
kNotInSynchronizationOfLazyAttribute);
|
||||
AttributeModificationReason::kDirectly);
|
||||
}
|
||||
|
||||
wtf_size_t Element::FindAttributeIndex(const QualifiedName& name) {
|
||||
if (GetElementData())
|
||||
return GetElementData()->Attributes().FindIndex(name);
|
||||
return kNotFound;
|
||||
}
|
||||
|
||||
ALWAYS_INLINE void Element::SetAttributeInternal(
|
||||
wtf_size_t index,
|
||||
const QualifiedName& name,
|
||||
const AtomicString& new_value,
|
||||
SynchronizationOfLazyAttribute in_synchronization_of_lazy_attribute) {
|
||||
AttributeModificationReason reason) {
|
||||
if (new_value.IsNull()) {
|
||||
if (index != kNotFound)
|
||||
RemoveAttributeInternal(index, in_synchronization_of_lazy_attribute);
|
||||
RemoveAttributeInternal(index, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index == kNotFound) {
|
||||
AppendAttributeInternal(name, new_value,
|
||||
in_synchronization_of_lazy_attribute);
|
||||
AppendAttributeInternal(name, new_value, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -8303,15 +8324,17 @@ ALWAYS_INLINE void Element::SetAttributeInternal(
|
||||
AtomicString existing_attribute_value = existing_attribute.Value();
|
||||
QualifiedName existing_attribute_name = existing_attribute.GetName();
|
||||
|
||||
if (!in_synchronization_of_lazy_attribute) {
|
||||
if (reason !=
|
||||
AttributeModificationReason::kBySynchronizationOfLazyAttribute) {
|
||||
WillModifyAttribute(existing_attribute_name, existing_attribute_value,
|
||||
new_value);
|
||||
}
|
||||
if (new_value != existing_attribute_value)
|
||||
EnsureUniqueElementData().Attributes().at(index).SetValue(new_value);
|
||||
if (!in_synchronization_of_lazy_attribute) {
|
||||
if (reason !=
|
||||
AttributeModificationReason::kBySynchronizationOfLazyAttribute) {
|
||||
DidModifyAttribute(existing_attribute_name, existing_attribute_value,
|
||||
new_value);
|
||||
new_value, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8375,7 +8398,7 @@ Attr* Element::setAttributeNode(Attr* attr_node,
|
||||
}
|
||||
|
||||
SetAttributeInternal(index, attr_node->GetQualifiedName(), value,
|
||||
kNotInSynchronizationOfLazyAttribute);
|
||||
AttributeModificationReason::kDirectly);
|
||||
|
||||
attr_node->AttachToElement(this, local_name);
|
||||
GetTreeScope().AdoptIfNeeded(*attr_node);
|
||||
@ -8397,7 +8420,7 @@ void Element::RemoveAttributeHinted(const AtomicString& name,
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveAttributeInternal(index, kNotInSynchronizationOfLazyAttribute);
|
||||
RemoveAttributeInternal(index, AttributeModificationReason::kDirectly);
|
||||
}
|
||||
|
||||
bool Element::IsDocumentElement() const {
|
||||
|
27
third_party/blink/renderer/core/dom/element.h
vendored
27
third_party/blink/renderer/core/dom/element.h
vendored
@ -55,6 +55,7 @@
|
||||
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
|
||||
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
|
||||
#include "third_party/blink/renderer/platform/wtf/text/atomic_string_table.h"
|
||||
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
|
||||
#include "ui/gfx/geometry/rect_f.h"
|
||||
|
||||
namespace gfx {
|
||||
@ -516,7 +517,13 @@ class CORE_EXPORT Element : public ContainerNode, public Animatable {
|
||||
NamedNodeMap* attributesForBindings() const;
|
||||
Vector<AtomicString> getAttributeNames() const;
|
||||
|
||||
enum class AttributeModificationReason { kDirectly, kByParser, kByCloning };
|
||||
enum class AttributeModificationReason {
|
||||
kDirectly,
|
||||
kByParser,
|
||||
kByCloning,
|
||||
kByMoveToNewDocument,
|
||||
kBySynchronizationOfLazyAttribute
|
||||
};
|
||||
struct AttributeModificationParams {
|
||||
STACK_ALLOCATED();
|
||||
|
||||
@ -1370,18 +1377,14 @@ class CORE_EXPORT Element : public ContainerNode, public Animatable {
|
||||
virtual void DidAddUserAgentShadowRoot(ShadowRoot&) {}
|
||||
virtual bool AlwaysCreateUserAgentShadowRoot() const { return false; }
|
||||
|
||||
enum SynchronizationOfLazyAttribute {
|
||||
kNotInSynchronizationOfLazyAttribute = 0,
|
||||
kInSynchronizationOfLazyAttribute
|
||||
};
|
||||
|
||||
void DidAddAttribute(const QualifiedName&, const AtomicString&);
|
||||
void WillModifyAttribute(const QualifiedName&,
|
||||
const AtomicString& old_value,
|
||||
const AtomicString& new_value);
|
||||
void DidModifyAttribute(const QualifiedName&,
|
||||
const AtomicString& old_value,
|
||||
const AtomicString& new_value);
|
||||
const AtomicString& new_value,
|
||||
AttributeModificationReason reason);
|
||||
void DidRemoveAttribute(const QualifiedName&, const AtomicString& old_value);
|
||||
|
||||
void SynchronizeAllAttributes() const;
|
||||
@ -1402,15 +1405,17 @@ class CORE_EXPORT Element : public ContainerNode, public Animatable {
|
||||
NodeType getNodeType() const final;
|
||||
bool ChildTypeAllowed(NodeType) const final;
|
||||
|
||||
// Returns the attribute's index or `kNotFound` if not found.
|
||||
wtf_size_t FindAttributeIndex(const QualifiedName&);
|
||||
|
||||
void SetAttributeInternal(wtf_size_t index,
|
||||
const QualifiedName&,
|
||||
const AtomicString& value,
|
||||
SynchronizationOfLazyAttribute);
|
||||
AttributeModificationReason);
|
||||
void AppendAttributeInternal(const QualifiedName&,
|
||||
const AtomicString& value,
|
||||
SynchronizationOfLazyAttribute);
|
||||
void RemoveAttributeInternal(wtf_size_t index,
|
||||
SynchronizationOfLazyAttribute);
|
||||
AttributeModificationReason);
|
||||
void RemoveAttributeInternal(wtf_size_t index, AttributeModificationReason);
|
||||
SpecificTrustedType ExpectedTrustedTypeForAttribute(
|
||||
const QualifiedName&) const;
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "third_party/blink/public/web/web_plugin.h"
|
||||
#include "third_party/blink/renderer/core/css/css_style_declaration.h"
|
||||
#include "third_party/blink/renderer/core/dom/document.h"
|
||||
#include "third_party/blink/renderer/core/dom/dom_token_list.h"
|
||||
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
|
||||
@ -21,6 +22,7 @@
|
||||
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
|
||||
#include "third_party/blink/renderer/core/paint/paint_layer.h"
|
||||
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
|
||||
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
|
||||
#include "third_party/blink/renderer/platform/bindings/script_forbidden_scope.h"
|
||||
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
|
||||
|
||||
@ -1110,4 +1112,23 @@ TEST_F(ElementTest, ParseFocusgroupAttrValueClearedAfterNodeRemoved) {
|
||||
ASSERT_EQ(fg2_flags, FocusgroupFlags::kNone);
|
||||
}
|
||||
|
||||
TEST_F(ElementTest, MixStyleAttributeAndCSSOMChanges) {
|
||||
Document& document = GetDocument();
|
||||
SetBodyContent(R"HTML(
|
||||
<div id="elmt" style="color: green;"></div>
|
||||
)HTML");
|
||||
|
||||
Element* elmt = document.getElementById("elmt");
|
||||
elmt->style()->setProperty(GetDocument().GetExecutionContext(), "color",
|
||||
"red", String(), ASSERT_NO_EXCEPTION);
|
||||
|
||||
// Verify that setting the style attribute back to its initial value is not
|
||||
// mistakenly considered as a no-op attribute change and ignored. It would be
|
||||
// without proper synchronization of attributes.
|
||||
elmt->setAttribute(html_names::kStyleAttr, "color: green;");
|
||||
|
||||
EXPECT_EQ(elmt->getAttribute(html_names::kStyleAttr), "color: green;");
|
||||
EXPECT_EQ(elmt->style()->getPropertyValue("color"), "green");
|
||||
}
|
||||
|
||||
} // namespace blink
|
||||
|
Reference in New Issue
Block a user