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 @@
X(TRACE_DISABLED_BY_DEFAULT("histogram_samples")) \
X(TRACE_DISABLED_BY_DEFAULT("java-heap-profiler")) \
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("loading")) \
X(TRACE_DISABLED_BY_DEFAULT("memory-infra")) \
......
......@@ -39,6 +39,14 @@ std::vector<double> LayoutShiftScores(TraceAnalyzer& analyzer) {
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
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(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 @@
#include "cc/layers/picture_layer.h"
#include "cc/trees/layer_tree_host.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_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
......@@ -29,18 +30,18 @@ namespace blink {
using ReattachHook = LayoutShiftTracker::ReattachHook;
static ReattachHook& GetReattachHook() {
namespace {
ReattachHook& GetReattachHook() {
DEFINE_STATIC_LOCAL(Persistent<ReattachHook>, hook,
(MakeGarbageCollected<ReattachHook>()));
return *hook;
}
static constexpr base::TimeDelta kTimerDelay =
base::TimeDelta::FromMilliseconds(500);
static const float kMovementThreshold = 3.0; // CSS pixels.
constexpr base::TimeDelta kTimerDelay = base::TimeDelta::FromMilliseconds(500);
const float kMovementThreshold = 3.0; // CSS pixels.
static FloatPoint LogicalStart(const FloatRect& rect,
const LayoutObject& object) {
FloatPoint LogicalStart(const FloatRect& rect, const LayoutObject& object) {
const ComputedStyle* style = object.Style();
DCHECK(style);
auto logical =
......@@ -49,59 +50,67 @@ static FloatPoint LogicalStart(const FloatRect& rect,
return FloatPoint(logical.InlineStart(), logical.BlockStart());
}
static float GetMoveDistance(const FloatRect& old_rect,
const FloatRect& new_rect,
const LayoutObject& object) {
float GetMoveDistance(const FloatRect& old_rect,
const FloatRect& new_rect,
const LayoutObject& object) {
FloatSize location_delta =
LogicalStart(new_rect, object) - LogicalStart(old_rect, object);
return std::max(fabs(location_delta.Width()), fabs(location_delta.Height()));
}
static bool EqualWithinMovementThreshold(const FloatPoint& a,
const FloatPoint& b,
const LayoutObject& object) {
bool EqualWithinMovementThreshold(const FloatPoint& a,
const FloatPoint& b,
const LayoutObject& object) {
float threshold_physical_px =
kMovementThreshold * object.StyleRef().EffectiveZoom();
return fabs(a.X() - b.X()) < 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
// pixel boundaries. Ignore rects smaller than half a pixel.
return rect.Width() < 0.5 || rect.Height() < 0.5;
}
static const PropertyTreeState PropertyTreeStateFor(
const LayoutObject& object) {
const PropertyTreeState PropertyTreeStateFor(const LayoutObject& object) {
return object.FirstFragment().LocalBorderBoxProperties();
}
static void RegionToTracedValue(const LayoutShiftRegion& region,
TracedValue& value) {
void RectToTracedValue(const IntRect& rect,
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;
for (IntRect rect : region.GetRects())
blink_region.Unite(Region(rect));
value.BeginArray("region_rects");
for (const IntRect& rect : blink_region.Rects()) {
value.BeginArray();
value.PushInteger(rect.X());
value.PushInteger(rect.Y());
value.PushInteger(rect.Width());
value.PushInteger(rect.Height());
value.EndArray();
}
for (const IntRect& rect : blink_region.Rects())
RectToTracedValue(rect, value);
value.EndArray();
}
#if DCHECK_IS_ON()
static bool ShouldLog(const LocalFrame& frame) {
bool ShouldLog(const LocalFrame& frame) {
const String& url = frame.GetDocument()->Url().GetString();
return !url.StartsWith("chrome-devtools:") && !url.StartsWith("devtools:");
}
#endif
} // namespace
LayoutShiftTracker::LayoutShiftTracker(LocalFrameView* frame_view)
: frame_view_(frame_view),
score_(0.0),
......@@ -209,6 +218,65 @@ void LayoutShiftTracker::ObjectShifted(
region_.AddRect(visible_old_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(
......@@ -297,6 +365,7 @@ void LayoutShiftTracker::NotifyPrePaintFinished() {
frame_max_distance_ = 0.0;
frame_scroll_delta_ = ScrollOffset();
attributions_.fill(Attribution());
}
void LayoutShiftTracker::ReportShift(double score_delta,
......@@ -423,9 +492,27 @@ std::unique_ptr<TracedValue> LayoutShiftTracker::PerFrameTraceData(
RegionToTracedValue(region_, *value);
value->SetBoolean("is_main_frame", frame_view_->GetFrame().IsMainFrame());
value->SetBoolean("had_recent_input", input_detected);
AttributionsToTracedValue(*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) {
// Store the layout shift rects in the HUD layer.
auto* cc_layer = frame_view_->RootCcLayer();
......
......@@ -10,6 +10,7 @@
#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/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/wtf/allocator/allocator.h"
......@@ -95,6 +96,7 @@ class CORE_EXPORT LayoutShiftTracker {
void TimerFired(TimerBase*) {}
std::unique_ptr<TracedValue> PerFrameTraceData(double score_delta,
bool input_detected) const;
void AttributionsToTracedValue(TracedValue&) const;
double SubframeWeightingFactor() const;
void SetLayoutShiftRects(const Vector<IntRect>& int_rects);
void UpdateInputTimestamp(base::TimeTicks timestamp);
......@@ -157,6 +159,29 @@ class CORE_EXPORT LayoutShiftTracker {
// User input includes window resizing but not scrolling.
base::TimeTicks most_recent_input_timestamp_;
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
......
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