Commit 2a3516fe authored by Steve Kobes's avatar Steve Kobes Committed by Commit Bot

Include top 5 shifted nodes in LayoutShift trace event.

When the layout_shift.debug trace category is enabled, the LayoutShift
trace event includes an impacted_nodes property containing the node ID
and the old and new visual rects for up to 5 shifted elements, selected
on the basis of their contribution to the impact region.

Bug: 1046212
Change-Id: I5d1833fbf831f0894b7036b73fa3c242b695ed64
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2042674Reviewed-by: default avatarNicolás Peña Moreno <npm@chromium.org>
Reviewed-by: default avataroysteine <oysteine@chromium.org>
Commit-Queue: Steve Kobes <skobes@chromium.org>
Cr-Commit-Position: refs/heads/master@{#747871}
parent d1d25656
...@@ -189,6 +189,7 @@ ...@@ -189,6 +189,7 @@
X(TRACE_DISABLED_BY_DEFAULT("histogram_samples")) \ X(TRACE_DISABLED_BY_DEFAULT("histogram_samples")) \
X(TRACE_DISABLED_BY_DEFAULT("java-heap-profiler")) \ X(TRACE_DISABLED_BY_DEFAULT("java-heap-profiler")) \
X(TRACE_DISABLED_BY_DEFAULT("layer-element")) \ X(TRACE_DISABLED_BY_DEFAULT("layer-element")) \
X(TRACE_DISABLED_BY_DEFAULT("layout_shift.debug")) \
X(TRACE_DISABLED_BY_DEFAULT("lifecycles")) \ X(TRACE_DISABLED_BY_DEFAULT("lifecycles")) \
X(TRACE_DISABLED_BY_DEFAULT("loading")) \ X(TRACE_DISABLED_BY_DEFAULT("loading")) \
X(TRACE_DISABLED_BY_DEFAULT("memory-infra")) \ X(TRACE_DISABLED_BY_DEFAULT("memory-infra")) \
......
...@@ -39,6 +39,14 @@ std::vector<double> LayoutShiftScores(TraceAnalyzer& analyzer) { ...@@ -39,6 +39,14 @@ std::vector<double> LayoutShiftScores(TraceAnalyzer& analyzer) {
return scores; return scores;
} }
void CheckRect(const Value& list_value, int x, int y, int width, int height) {
auto list = list_value.GetList();
EXPECT_EQ(list[0].GetInt(), x);
EXPECT_EQ(list[1].GetInt(), y);
EXPECT_EQ(list[2].GetInt(), width);
EXPECT_EQ(list[3].GetInt(), height);
}
} // namespace } // namespace
IN_PROC_BROWSER_TEST_F(MetricIntegrationTest, LayoutInstability) { IN_PROC_BROWSER_TEST_F(MetricIntegrationTest, LayoutInstability) {
...@@ -66,3 +74,128 @@ IN_PROC_BROWSER_TEST_F(MetricIntegrationTest, LayoutInstability) { ...@@ -66,3 +74,128 @@ IN_PROC_BROWSER_TEST_F(MetricIntegrationTest, LayoutInstability) {
EXPECT_EQ(1ul, samples.size()); EXPECT_EQ(1ul, samples.size());
EXPECT_EQ(samples[0], Bucket(LayoutShiftUmaValue(expected_score), 1)); EXPECT_EQ(samples[0], Bucket(LayoutShiftUmaValue(expected_score), 1));
} }
IN_PROC_BROWSER_TEST_F(MetricIntegrationTest, CLSAttribution_Enclosure) {
LoadHTML(R"HTML(
<script src="/layout-instability/resources/util.js"></script>
<style>
body { margin: 0; }
#shifter {
position: relative; background: #def;
width: 300px; height: 200px;
}
#inner {
position: relative; background: #f97;
width: 100px; height: 100px;
}
#absfollow {
position: absolute; background: #ffd; opacity: 50%;
width: 350px; height: 200px; left: 0; top: 160px;
}
.stateB { top: 160px; }
.stateB #inner { left: 100px; }
.stateC ~ #absfollow { top: 0; }
</style>
<div id="shifter" class="stateA">
<div id="inner"></div>
</div>
<div id="absfollow"></div>
<script>
runTest = async () => {
await waitForAnimationFrames(2);
document.querySelector("#shifter").className = "stateB";
await waitForAnimationFrames(2);
document.querySelector("#shifter").className = "stateC";
await waitForAnimationFrames(2);
};
</script>
)HTML");
StartTracing({"loading", TRACE_DISABLED_BY_DEFAULT("layout_shift.debug")});
ASSERT_TRUE(EvalJs(web_contents(), "runTest()").error.empty());
std::unique_ptr<TraceAnalyzer> analyzer = StopTracingAndAnalyze();
TraceEventVector events;
analyzer->FindEvents(Query::EventNameIs("LayoutShift"), &events);
EXPECT_EQ(2ul, events.size());
// Shift of #inner ignored as redundant, fully enclosed by #shifter.
std::unique_ptr<Value> shift_data1;
events[0]->GetArgAsValue("data", &shift_data1);
auto impacted_nodes1 = shift_data1->FindListKey("impacted_nodes")->GetList();
EXPECT_EQ(1ul, impacted_nodes1.size());
const Value& node_data1 = impacted_nodes1[0];
EXPECT_NE(*node_data1.FindIntKey("node_id"), 0);
CheckRect(*node_data1.FindListKey("old_rect"), 0, 0, 300, 200);
CheckRect(*node_data1.FindListKey("new_rect"), 0, 160, 300, 200);
// Shift of #shifter ignored as redundant, fully enclosed by #follow.
std::unique_ptr<Value> shift_data2;
events[1]->GetArgAsValue("data", &shift_data2);
auto impacted_nodes2 = shift_data2->FindListKey("impacted_nodes")->GetList();
EXPECT_EQ(1ul, impacted_nodes2.size());
const Value& node_data2 = impacted_nodes2[0];
EXPECT_NE(*node_data2.FindIntKey("node_id"), 0);
CheckRect(*node_data2.FindListKey("old_rect"), 0, 160, 350, 200);
CheckRect(*node_data2.FindListKey("new_rect"), 0, 0, 350, 200);
}
IN_PROC_BROWSER_TEST_F(MetricIntegrationTest, CLSAttribution_MaxImpact) {
LoadHTML(R"HTML(
<script src="/layout-instability/resources/util.js"></script>
<style>
body { margin: 0; }
#a, #b, #c, #d, #e, #f {
display: inline-block;
background: gray;
min-width: 10px;
min-height: 10px;
vertical-align: top;
}
#a { width: 30px; height: 30px; }
#b { width: 20px; height: 20px; }
#c { height: 50px; }
#d { width: 50px; }
#e { width: 40px; height: 30px; }
#f { width: 30px; height: 40px; }
</style>
<div id="grow"></div>
<div id="a"></div
><div id="b"></div
><div id="c"></div
><div id="d"></div
><div id="e"></div
><div id="f"></div>
<script>
runTest = async () => {
await waitForAnimationFrames(2);
document.querySelector("#grow").style.height = "50px";
await waitForAnimationFrames(2);
};
</script>
)HTML");
StartTracing({"loading", TRACE_DISABLED_BY_DEFAULT("layout_shift.debug")});
ASSERT_TRUE(EvalJs(web_contents(), "runTest()").error.empty());
std::unique_ptr<TraceAnalyzer> analyzer = StopTracingAndAnalyze();
TraceEventVector events;
analyzer->FindEvents(Query::EventNameIs("LayoutShift"), &events);
EXPECT_EQ(1ul, events.size());
std::unique_ptr<Value> shift_data;
events[0]->GetArgAsValue("data", &shift_data);
auto impacted = shift_data->FindListKey("impacted_nodes")->GetList();
EXPECT_EQ(5ul, impacted.size());
// #f should replace #b, the smallest div.
CheckRect(*impacted[0].FindListKey("new_rect"), 0, 50, 30, 30); // #a
CheckRect(*impacted[1].FindListKey("new_rect"), 150, 50, 30, 40); // #f
CheckRect(*impacted[2].FindListKey("new_rect"), 50, 50, 10, 50); // #c
CheckRect(*impacted[3].FindListKey("new_rect"), 60, 50, 50, 10); // #d
CheckRect(*impacted[4].FindListKey("new_rect"), 110, 50, 40, 30); // #e
}
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "cc/layers/picture_layer.h" #include "cc/layers/picture_layer.h"
#include "cc/trees/layer_tree_host.h" #include "cc/trees/layer_tree_host.h"
#include "third_party/blink/public/common/input/web_pointer_event.h" #include "third_party/blink/public/common/input/web_pointer_event.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h" #include "third_party/blink/renderer/core/frame/local_frame_client.h"
...@@ -29,18 +30,18 @@ namespace blink { ...@@ -29,18 +30,18 @@ namespace blink {
using ReattachHook = LayoutShiftTracker::ReattachHook; using ReattachHook = LayoutShiftTracker::ReattachHook;
static ReattachHook& GetReattachHook() { namespace {
ReattachHook& GetReattachHook() {
DEFINE_STATIC_LOCAL(Persistent<ReattachHook>, hook, DEFINE_STATIC_LOCAL(Persistent<ReattachHook>, hook,
(MakeGarbageCollected<ReattachHook>())); (MakeGarbageCollected<ReattachHook>()));
return *hook; return *hook;
} }
static constexpr base::TimeDelta kTimerDelay = constexpr base::TimeDelta kTimerDelay = base::TimeDelta::FromMilliseconds(500);
base::TimeDelta::FromMilliseconds(500); const float kMovementThreshold = 3.0; // CSS pixels.
static const float kMovementThreshold = 3.0; // CSS pixels.
static FloatPoint LogicalStart(const FloatRect& rect, FloatPoint LogicalStart(const FloatRect& rect, const LayoutObject& object) {
const LayoutObject& object) {
const ComputedStyle* style = object.Style(); const ComputedStyle* style = object.Style();
DCHECK(style); DCHECK(style);
auto logical = auto logical =
...@@ -49,59 +50,67 @@ static FloatPoint LogicalStart(const FloatRect& rect, ...@@ -49,59 +50,67 @@ static FloatPoint LogicalStart(const FloatRect& rect,
return FloatPoint(logical.InlineStart(), logical.BlockStart()); return FloatPoint(logical.InlineStart(), logical.BlockStart());
} }
static float GetMoveDistance(const FloatRect& old_rect, float GetMoveDistance(const FloatRect& old_rect,
const FloatRect& new_rect, const FloatRect& new_rect,
const LayoutObject& object) { const LayoutObject& object) {
FloatSize location_delta = FloatSize location_delta =
LogicalStart(new_rect, object) - LogicalStart(old_rect, object); LogicalStart(new_rect, object) - LogicalStart(old_rect, object);
return std::max(fabs(location_delta.Width()), fabs(location_delta.Height())); return std::max(fabs(location_delta.Width()), fabs(location_delta.Height()));
} }
static bool EqualWithinMovementThreshold(const FloatPoint& a, bool EqualWithinMovementThreshold(const FloatPoint& a,
const FloatPoint& b, const FloatPoint& b,
const LayoutObject& object) { const LayoutObject& object) {
float threshold_physical_px = float threshold_physical_px =
kMovementThreshold * object.StyleRef().EffectiveZoom(); kMovementThreshold * object.StyleRef().EffectiveZoom();
return fabs(a.X() - b.X()) < threshold_physical_px && return fabs(a.X() - b.X()) < threshold_physical_px &&
fabs(a.Y() - b.Y()) < threshold_physical_px; fabs(a.Y() - b.Y()) < threshold_physical_px;
} }
static bool SmallerThanRegionGranularity(const FloatRect& rect) { bool SmallerThanRegionGranularity(const FloatRect& rect) {
// The region uses integer coordinates, so the rects are snapped to // The region uses integer coordinates, so the rects are snapped to
// pixel boundaries. Ignore rects smaller than half a pixel. // pixel boundaries. Ignore rects smaller than half a pixel.
return rect.Width() < 0.5 || rect.Height() < 0.5; return rect.Width() < 0.5 || rect.Height() < 0.5;
} }
static const PropertyTreeState PropertyTreeStateFor( const PropertyTreeState PropertyTreeStateFor(const LayoutObject& object) {
const LayoutObject& object) {
return object.FirstFragment().LocalBorderBoxProperties(); return object.FirstFragment().LocalBorderBoxProperties();
} }
static void RegionToTracedValue(const LayoutShiftRegion& region, void RectToTracedValue(const IntRect& rect,
TracedValue& value) { TracedValue& value,
const char* key = nullptr) {
if (key)
value.BeginArray(key);
else
value.BeginArray();
value.PushInteger(rect.X());
value.PushInteger(rect.Y());
value.PushInteger(rect.Width());
value.PushInteger(rect.Height());
value.EndArray();
}
void RegionToTracedValue(const LayoutShiftRegion& region, TracedValue& value) {
Region blink_region; Region blink_region;
for (IntRect rect : region.GetRects()) for (IntRect rect : region.GetRects())
blink_region.Unite(Region(rect)); blink_region.Unite(Region(rect));
value.BeginArray("region_rects"); value.BeginArray("region_rects");
for (const IntRect& rect : blink_region.Rects()) { for (const IntRect& rect : blink_region.Rects())
value.BeginArray(); RectToTracedValue(rect, value);
value.PushInteger(rect.X());
value.PushInteger(rect.Y());
value.PushInteger(rect.Width());
value.PushInteger(rect.Height());
value.EndArray();
}
value.EndArray(); value.EndArray();
} }
#if DCHECK_IS_ON() #if DCHECK_IS_ON()
static bool ShouldLog(const LocalFrame& frame) { bool ShouldLog(const LocalFrame& frame) {
const String& url = frame.GetDocument()->Url().GetString(); const String& url = frame.GetDocument()->Url().GetString();
return !url.StartsWith("chrome-devtools:") && !url.StartsWith("devtools:"); return !url.StartsWith("chrome-devtools:") && !url.StartsWith("devtools:");
} }
#endif #endif
} // namespace
LayoutShiftTracker::LayoutShiftTracker(LocalFrameView* frame_view) LayoutShiftTracker::LayoutShiftTracker(LocalFrameView* frame_view)
: frame_view_(frame_view), : frame_view_(frame_view),
score_(0.0), score_(0.0),
...@@ -209,6 +218,65 @@ void LayoutShiftTracker::ObjectShifted( ...@@ -209,6 +218,65 @@ void LayoutShiftTracker::ObjectShifted(
region_.AddRect(visible_old_rect); region_.AddRect(visible_old_rect);
region_.AddRect(visible_new_rect); region_.AddRect(visible_new_rect);
bool should_trace_nodes;
TRACE_EVENT_CATEGORY_GROUP_ENABLED(
TRACE_DISABLED_BY_DEFAULT("layout_shift.debug"), &should_trace_nodes);
if (should_trace_nodes) {
if (Node* node = source.GetNode()) {
MaybeRecordAttribution(
{DOMNodeIds::IdForNode(node), visible_old_rect, visible_new_rect});
}
}
}
LayoutShiftTracker::Attribution::Attribution() : node_id(kInvalidDOMNodeId) {}
LayoutShiftTracker::Attribution::Attribution(DOMNodeId node_id_arg,
IntRect old_visual_rect_arg,
IntRect new_visual_rect_arg)
: node_id(node_id_arg),
old_visual_rect(old_visual_rect_arg),
new_visual_rect(new_visual_rect_arg) {}
LayoutShiftTracker::Attribution::operator bool() const {
return node_id != kInvalidDOMNodeId;
}
bool LayoutShiftTracker::Attribution::Encloses(const Attribution& other) const {
return old_visual_rect.Contains(other.old_visual_rect) &&
new_visual_rect.Contains(other.new_visual_rect);
}
int LayoutShiftTracker::Attribution::Area() const {
int old_area = old_visual_rect.Width() * old_visual_rect.Height();
int new_area = new_visual_rect.Width() * new_visual_rect.Height();
IntRect intersection = Intersection(old_visual_rect, new_visual_rect);
int shared_area = intersection.Width() * intersection.Height();
return old_area + new_area - shared_area;
}
bool LayoutShiftTracker::Attribution::MoreImpactfulThan(
const Attribution& other) const {
return Area() > other.Area();
}
void LayoutShiftTracker::MaybeRecordAttribution(
const Attribution& attribution) {
Attribution* smallest = nullptr;
for (auto& slot : attributions_) {
if (!slot || attribution.Encloses(slot)) {
slot = attribution;
return;
}
if (slot.Encloses(attribution))
return;
if (!smallest || smallest->MoreImpactfulThan(slot))
smallest = &slot;
}
// No empty slots or redundancies. Replace smallest existing slot if larger.
if (attribution.MoreImpactfulThan(*smallest))
*smallest = attribution;
} }
void LayoutShiftTracker::NotifyObjectPrePaint( void LayoutShiftTracker::NotifyObjectPrePaint(
...@@ -297,6 +365,7 @@ void LayoutShiftTracker::NotifyPrePaintFinished() { ...@@ -297,6 +365,7 @@ void LayoutShiftTracker::NotifyPrePaintFinished() {
frame_max_distance_ = 0.0; frame_max_distance_ = 0.0;
frame_scroll_delta_ = ScrollOffset(); frame_scroll_delta_ = ScrollOffset();
attributions_.fill(Attribution());
} }
void LayoutShiftTracker::ReportShift(double score_delta, void LayoutShiftTracker::ReportShift(double score_delta,
...@@ -423,9 +492,27 @@ std::unique_ptr<TracedValue> LayoutShiftTracker::PerFrameTraceData( ...@@ -423,9 +492,27 @@ std::unique_ptr<TracedValue> LayoutShiftTracker::PerFrameTraceData(
RegionToTracedValue(region_, *value); RegionToTracedValue(region_, *value);
value->SetBoolean("is_main_frame", frame_view_->GetFrame().IsMainFrame()); value->SetBoolean("is_main_frame", frame_view_->GetFrame().IsMainFrame());
value->SetBoolean("had_recent_input", input_detected); value->SetBoolean("had_recent_input", input_detected);
AttributionsToTracedValue(*value);
return value; return value;
} }
void LayoutShiftTracker::AttributionsToTracedValue(TracedValue& value) const {
const Attribution* it = attributions_.begin();
if (!*it)
return;
value.BeginArray("impacted_nodes");
while (it != attributions_.end() && it->node_id != kInvalidDOMNodeId) {
value.BeginDictionary();
value.SetInteger("node_id", it->node_id);
RectToTracedValue(it->old_visual_rect, value, "old_rect");
RectToTracedValue(it->new_visual_rect, value, "new_rect");
value.EndDictionary();
it++;
}
value.EndArray();
}
void LayoutShiftTracker::SetLayoutShiftRects(const Vector<IntRect>& int_rects) { void LayoutShiftTracker::SetLayoutShiftRects(const Vector<IntRect>& int_rects) {
// Store the layout shift rects in the HUD layer. // Store the layout shift rects in the HUD layer.
auto* cc_layer = frame_view_->RootCcLayer(); auto* cc_layer = frame_view_->RootCcLayer();
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
#include "third_party/blink/renderer/core/layout/layout_shift_region.h" #include "third_party/blink/renderer/core/layout/layout_shift_region.h"
#include "third_party/blink/renderer/core/scroll/scroll_types.h" #include "third_party/blink/renderer/core/scroll/scroll_types.h"
#include "third_party/blink/renderer/platform/geometry/region.h" #include "third_party/blink/renderer/platform/geometry/region.h"
#include "third_party/blink/renderer/platform/graphics/dom_node_id.h"
#include "third_party/blink/renderer/platform/timer.h" #include "third_party/blink/renderer/platform/timer.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
...@@ -95,6 +96,7 @@ class CORE_EXPORT LayoutShiftTracker { ...@@ -95,6 +96,7 @@ class CORE_EXPORT LayoutShiftTracker {
void TimerFired(TimerBase*) {} void TimerFired(TimerBase*) {}
std::unique_ptr<TracedValue> PerFrameTraceData(double score_delta, std::unique_ptr<TracedValue> PerFrameTraceData(double score_delta,
bool input_detected) const; bool input_detected) const;
void AttributionsToTracedValue(TracedValue&) const;
double SubframeWeightingFactor() const; double SubframeWeightingFactor() const;
void SetLayoutShiftRects(const Vector<IntRect>& int_rects); void SetLayoutShiftRects(const Vector<IntRect>& int_rects);
void UpdateInputTimestamp(base::TimeTicks timestamp); void UpdateInputTimestamp(base::TimeTicks timestamp);
...@@ -157,6 +159,29 @@ class CORE_EXPORT LayoutShiftTracker { ...@@ -157,6 +159,29 @@ class CORE_EXPORT LayoutShiftTracker {
// User input includes window resizing but not scrolling. // User input includes window resizing but not scrolling.
base::TimeTicks most_recent_input_timestamp_; base::TimeTicks most_recent_input_timestamp_;
bool most_recent_input_timestamp_initialized_; bool most_recent_input_timestamp_initialized_;
struct Attribution {
DOMNodeId node_id;
IntRect old_visual_rect;
IntRect new_visual_rect;
Attribution();
Attribution(DOMNodeId node_id,
IntRect old_visual_rect,
IntRect new_visual_rect);
explicit operator bool() const;
bool Encloses(const Attribution&) const;
bool MoreImpactfulThan(const Attribution&) const;
int Area() const;
};
static constexpr int kMaxAttributions = 5;
void MaybeRecordAttribution(const Attribution&);
// Nodes that have contributed to the impact region for the current frame, for
// use in trace event. Only populated while tracing.
std::array<Attribution, kMaxAttributions> attributions_;
}; };
} // namespace blink } // namespace blink
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment