summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/core/layout/layout_shift_tracker.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/third_party/blink/renderer/core/layout/layout_shift_tracker.cc')
-rw-r--r--chromium/third_party/blink/renderer/core/layout/layout_shift_tracker.cc529
1 files changed, 529 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/core/layout/layout_shift_tracker.cc b/chromium/third_party/blink/renderer/core/layout/layout_shift_tracker.cc
new file mode 100644
index 00000000000..5c3b039122a
--- /dev/null
+++ b/chromium/third_party/blink/renderer/core/layout/layout_shift_tracker.cc
@@ -0,0 +1,529 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/layout/layout_shift_tracker.h"
+
+#include "cc/layers/heads_up_display_layer.h"
+#include "cc/layers/picture_layer.h"
+#include "third_party/blink/public/platform/web_pointer_event.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"
+#include "third_party/blink/renderer/core/frame/location.h"
+#include "third_party/blink/renderer/core/frame/visual_viewport.h"
+#include "third_party/blink/renderer/core/layout/layout_object.h"
+#include "third_party/blink/renderer/core/layout/layout_view.h"
+#include "third_party/blink/renderer/core/page/chrome_client.h"
+#include "third_party/blink/renderer/core/page/page.h"
+#include "third_party/blink/renderer/core/timing/dom_window_performance.h"
+#include "third_party/blink/renderer/core/timing/performance_entry.h"
+#include "third_party/blink/renderer/core/timing/window_performance.h"
+#include "third_party/blink/renderer/platform/graphics/graphics_layer.h"
+#include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+#include "ui/gfx/geometry/rect.h"
+
+namespace blink {
+
+static constexpr base::TimeDelta kTimerDelay =
+ base::TimeDelta::FromMilliseconds(500);
+static const float kRegionGranularitySteps = 60.0;
+// TODO: Vary by Finch experiment parameter.
+static const float kSweepLineRegionGranularity = 1.0;
+static const float kMovementThreshold = 3.0; // CSS pixels.
+
+static FloatPoint LogicalStart(const FloatRect& rect,
+ const LayoutObject& object) {
+ const ComputedStyle* style = object.Style();
+ DCHECK(style);
+ auto logical =
+ PhysicalToLogical<float>(style->GetWritingMode(), style->Direction(),
+ rect.Y(), rect.MaxX(), rect.MaxY(), rect.X());
+ return FloatPoint(logical.InlineStart(), logical.BlockStart());
+}
+
+static 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()));
+}
+
+float LayoutShiftTracker::RegionGranularityScale(
+ const IntRect& viewport) const {
+ if (RuntimeEnabledFeatures::JankTrackingSweepLineEnabled())
+ return kSweepLineRegionGranularity;
+
+ return kRegionGranularitySteps /
+ std::min(viewport.Height(), viewport.Width());
+}
+
+static 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,
+ float granularity_scale) {
+ return rect.Width() * granularity_scale < 0.5 ||
+ rect.Height() * granularity_scale < 0.5;
+}
+
+static const PropertyTreeState PropertyTreeStateFor(
+ const LayoutObject& object) {
+ return object.FirstFragment().LocalBorderBoxProperties();
+}
+
+static void RegionToTracedValue(const Region& region,
+ double granularity_scale,
+ TracedValue& value) {
+ value.BeginArray("region_rects");
+ for (const IntRect& rect : region.Rects()) {
+ value.BeginArray();
+ value.PushInteger(clampTo<int>(roundf(rect.X() / granularity_scale)));
+ value.PushInteger(clampTo<int>(roundf(rect.Y() / granularity_scale)));
+ value.PushInteger(clampTo<int>(roundf(rect.Width() / granularity_scale)));
+ value.PushInteger(clampTo<int>(roundf(rect.Height() / granularity_scale)));
+ value.EndArray();
+ }
+ value.EndArray();
+}
+
+static void RegionToTracedValue(const LayoutShiftRegion& region,
+ double granularity_scale,
+ TracedValue& value) {
+ Region old_region;
+ for (IntRect rect : region.GetRects())
+ old_region.Unite(Region(rect));
+ RegionToTracedValue(old_region, granularity_scale, value);
+}
+
+#if DCHECK_IS_ON()
+static bool ShouldLog(const LocalFrame& frame) {
+ const String& url = frame.GetDocument()->Url().GetString();
+ return !url.StartsWith("chrome-devtools:") && !url.StartsWith("devtools:");
+}
+#endif
+
+LayoutShiftTracker::LayoutShiftTracker(LocalFrameView* frame_view)
+ : frame_view_(frame_view),
+ score_(0.0),
+ weighted_score_(0.0),
+ timer_(frame_view->GetFrame().GetTaskRunner(TaskType::kInternalDefault),
+ this,
+ &LayoutShiftTracker::TimerFired),
+ frame_max_distance_(0.0),
+ overall_max_distance_(0.0),
+ observed_input_or_scroll_(false),
+ most_recent_input_timestamp_initialized_(false) {}
+
+void LayoutShiftTracker::ObjectShifted(
+ const LayoutObject& source,
+ const PropertyTreeState& property_tree_state,
+ FloatRect old_rect,
+ FloatRect new_rect) {
+ if (old_rect.IsEmpty() || new_rect.IsEmpty())
+ return;
+
+ if (EqualWithinMovementThreshold(LogicalStart(old_rect, source),
+ LogicalStart(new_rect, source), source))
+ return;
+
+ IntRect viewport =
+ IntRect(IntPoint(),
+ frame_view_->GetScrollableArea()->VisibleContentRect().Size());
+ float scale = RegionGranularityScale(viewport);
+
+ if (SmallerThanRegionGranularity(old_rect, scale) &&
+ SmallerThanRegionGranularity(new_rect, scale))
+ return;
+
+ // Ignore layout objects that move (in the coordinate space of the paint
+ // invalidation container) on scroll.
+ // TODO(skobes): Find a way to detect when these objects shift.
+ if (source.IsFixedPositioned() || source.IsStickyPositioned())
+ return;
+
+ // SVG elements don't participate in the normal layout algorithms and are
+ // more likely to be used for animations.
+ if (source.IsSVG())
+ return;
+
+ const auto root_state = PropertyTreeStateFor(*source.View());
+
+ FloatClipRect clip_rect =
+ GeometryMapper::LocalToAncestorClipRect(property_tree_state, root_state);
+
+ // If the clip region is empty, then the resulting layout shift isn't visible
+ // in the viewport so ignore it.
+ if (!clip_rect.IsInfinite() && clip_rect.Rect().IsEmpty())
+ return;
+
+ GeometryMapper::SourceToDestinationRect(property_tree_state.Transform(),
+ root_state.Transform(), old_rect);
+ GeometryMapper::SourceToDestinationRect(property_tree_state.Transform(),
+ root_state.Transform(), new_rect);
+
+ if (EqualWithinMovementThreshold(old_rect.Location(), new_rect.Location(),
+ source)) {
+ return;
+ }
+
+ if (EqualWithinMovementThreshold(old_rect.Location() + frame_scroll_delta_,
+ new_rect.Location(), source)) {
+ // TODO(skobes): Checking frame_scroll_delta_ is an imperfect solution to
+ // allowing counterscrolled layout shifts. Ideally, we would map old_rect
+ // to viewport coordinates using the previous frame's scroll tree.
+ return;
+ }
+
+ FloatRect clipped_old_rect(old_rect), clipped_new_rect(new_rect);
+ if (!clip_rect.IsInfinite()) {
+ clipped_old_rect.Intersect(clip_rect.Rect());
+ clipped_new_rect.Intersect(clip_rect.Rect());
+ }
+
+ IntRect visible_old_rect = RoundedIntRect(clipped_old_rect);
+ visible_old_rect.Intersect(viewport);
+ IntRect visible_new_rect = RoundedIntRect(clipped_new_rect);
+ visible_new_rect.Intersect(viewport);
+
+ if (visible_old_rect.IsEmpty() && visible_new_rect.IsEmpty())
+ return;
+
+ // Compute move distance based on unclipped rects, to accurately determine how
+ // much the element moved.
+ float move_distance = GetMoveDistance(old_rect, new_rect, source);
+ frame_max_distance_ = std::max(frame_max_distance_, move_distance);
+
+#if DCHECK_IS_ON()
+ LocalFrame& frame = frame_view_->GetFrame();
+ if (ShouldLog(frame)) {
+ DVLOG(2) << "in " << (frame.IsMainFrame() ? "" : "subframe ")
+ << frame.GetDocument()->Url().GetString() << ", "
+ << source.DebugName() << " moved from " << old_rect.ToString()
+ << " to " << new_rect.ToString() << " (visible from "
+ << visible_old_rect.ToString() << " to "
+ << visible_new_rect.ToString() << ")";
+ }
+#endif
+
+ visible_old_rect.Scale(scale);
+ visible_new_rect.Scale(scale);
+
+ if (RuntimeEnabledFeatures::JankTrackingSweepLineEnabled()) {
+ region_experimental_.AddRect(visible_old_rect);
+ region_experimental_.AddRect(visible_new_rect);
+ } else {
+ region_.Unite(Region(visible_old_rect));
+ region_.Unite(Region(visible_new_rect));
+ }
+}
+
+void LayoutShiftTracker::NotifyObjectPrePaint(
+ const LayoutObject& object,
+ const PropertyTreeState& property_tree_state,
+ const IntRect& old_visual_rect,
+ const IntRect& new_visual_rect) {
+ if (!IsActive())
+ return;
+
+ ObjectShifted(object, property_tree_state, FloatRect(old_visual_rect),
+ FloatRect(new_visual_rect));
+}
+
+void LayoutShiftTracker::NotifyCompositedLayerMoved(
+ const LayoutObject& layout_object,
+ FloatRect old_layer_rect,
+ FloatRect new_layer_rect) {
+ if (!IsActive())
+ return;
+
+ // Make sure we can access a transform node.
+ if (!layout_object.FirstFragment().HasLocalBorderBoxProperties())
+ return;
+
+ ObjectShifted(layout_object, PropertyTreeStateFor(layout_object),
+ old_layer_rect, new_layer_rect);
+}
+
+double LayoutShiftTracker::SubframeWeightingFactor() const {
+ LocalFrame& frame = frame_view_->GetFrame();
+ if (frame.IsMainFrame())
+ return 1;
+
+ // Map the subframe view rect into the coordinate space of the local root.
+ FloatClipRect subframe_cliprect =
+ FloatClipRect(FloatRect(FloatPoint(), FloatSize(frame_view_->Size())));
+ GeometryMapper::LocalToAncestorVisualRect(
+ frame_view_->GetLayoutView()->FirstFragment().LocalBorderBoxProperties(),
+ PropertyTreeState::Root(), subframe_cliprect);
+ auto subframe_rect = PhysicalRect::EnclosingRect(subframe_cliprect.Rect());
+
+ // Intersect with the portion of the local root that overlaps the main frame.
+ frame.LocalFrameRoot().View()->MapToVisualRectInTopFrameSpace(subframe_rect);
+ IntSize subframe_visible_size = subframe_rect.PixelSnappedSize();
+ IntSize main_frame_size = frame.GetPage()->GetVisualViewport().Size();
+
+ // TODO(crbug.com/940711): This comparison ignores page scale and CSS
+ // transforms above the local root.
+ return static_cast<double>(subframe_visible_size.Area()) /
+ main_frame_size.Area();
+}
+
+void LayoutShiftTracker::NotifyPrePaintFinished() {
+ if (!IsActive())
+ return;
+ bool use_sweep_line = RuntimeEnabledFeatures::JankTrackingSweepLineEnabled();
+ bool region_is_empty =
+ use_sweep_line ? region_experimental_.IsEmpty() : region_.IsEmpty();
+ if (region_is_empty)
+ return;
+
+ IntRect viewport = frame_view_->GetScrollableArea()->VisibleContentRect();
+ if (viewport.IsEmpty())
+ return;
+
+ double granularity_scale = RegionGranularityScale(viewport);
+ IntRect scaled_viewport = viewport;
+ scaled_viewport.Scale(granularity_scale);
+
+ double viewport_area =
+ double(scaled_viewport.Width()) * double(scaled_viewport.Height());
+ uint64_t region_area =
+ use_sweep_line ? region_experimental_.Area() : region_.Area();
+ double impact_fraction = region_area / viewport_area;
+ DCHECK_GT(impact_fraction, 0);
+
+ DCHECK_GT(frame_max_distance_, 0.0);
+ double viewport_max_dimension = std::max(viewport.Width(), viewport.Height());
+ double move_distance_factor =
+ (frame_max_distance_ < viewport_max_dimension)
+ ? double(frame_max_distance_) / viewport_max_dimension
+ : 1.0;
+ double score_delta = impact_fraction * move_distance_factor;
+ double weighted_score_delta = score_delta * SubframeWeightingFactor();
+
+ overall_max_distance_ = std::max(overall_max_distance_, frame_max_distance_);
+
+#if DCHECK_IS_ON()
+ LocalFrame& frame = frame_view_->GetFrame();
+ if (ShouldLog(frame)) {
+ DVLOG(1) << "in " << (frame.IsMainFrame() ? "" : "subframe ")
+ << frame.GetDocument()->Url().GetString() << ", viewport was "
+ << (impact_fraction * 100) << "% impacted with distance fraction "
+ << move_distance_factor;
+ }
+#endif
+
+ if (pointerdown_pending_data_.saw_pointerdown) {
+ pointerdown_pending_data_.score_delta += score_delta;
+ pointerdown_pending_data_.weighted_score_delta += weighted_score_delta;
+ } else {
+ ReportShift(score_delta, weighted_score_delta);
+ }
+
+ if (use_sweep_line) {
+ if (!region_experimental_.IsEmpty()) {
+ SetLayoutShiftRects(region_experimental_.GetRects(), 1, true);
+ }
+ region_experimental_.Reset();
+ } else {
+ if (!region_.IsEmpty()) {
+ SetLayoutShiftRects(region_.Rects(), granularity_scale, false);
+ }
+ region_ = Region();
+ }
+ frame_max_distance_ = 0.0;
+ frame_scroll_delta_ = ScrollOffset();
+}
+
+void LayoutShiftTracker::ReportShift(double score_delta,
+ double weighted_score_delta) {
+ LocalFrame& frame = frame_view_->GetFrame();
+ bool had_recent_input = timer_.IsActive();
+
+ if (!had_recent_input) {
+ score_ += score_delta;
+ if (weighted_score_delta > 0) {
+ weighted_score_ += weighted_score_delta;
+ frame.Client()->DidObserveLayoutShift(weighted_score_delta,
+ observed_input_or_scroll_);
+ }
+ }
+
+ if (RuntimeEnabledFeatures::LayoutInstabilityAPIEnabled(
+ frame.GetDocument()) &&
+ frame.DomWindow()) {
+ WindowPerformance* performance =
+ DOMWindowPerformance::performance(*frame.DomWindow());
+ if (performance) {
+ performance->AddLayoutJankFraction(score_delta, had_recent_input,
+ most_recent_input_timestamp_);
+ }
+ }
+
+ TRACE_EVENT_INSTANT2("loading", "LayoutShift", TRACE_EVENT_SCOPE_THREAD,
+ "data", PerFrameTraceData(score_delta, had_recent_input),
+ "frame", ToTraceValue(&frame));
+
+#if DCHECK_IS_ON()
+ if (ShouldLog(frame)) {
+ DVLOG(1) << "in " << (frame.IsMainFrame() ? "" : "subframe ")
+ << frame.GetDocument()->Url().GetString() << ", layout shift of "
+ << score_delta
+ << (had_recent_input ? " excluded by recent input" : " reported")
+ << "; cumulative score is " << score_;
+ }
+#endif
+}
+
+void LayoutShiftTracker::NotifyInput(const WebInputEvent& event) {
+ const WebInputEvent::Type type = event.GetType();
+ const bool saw_pointerdown = pointerdown_pending_data_.saw_pointerdown;
+ const bool pointerdown_became_tap =
+ saw_pointerdown && type == WebInputEvent::kPointerUp;
+ const bool event_type_stops_pointerdown_buffering =
+ type == WebInputEvent::kPointerUp ||
+ type == WebInputEvent::kPointerCausedUaAction ||
+ type == WebInputEvent::kPointerCancel;
+
+ // Only non-hovering pointerdown requires buffering.
+ const bool is_hovering_pointerdown =
+ type == WebInputEvent::kPointerDown &&
+ static_cast<const WebPointerEvent&>(event).hovering;
+
+ const bool should_trigger_shift_exclusion =
+ type == WebInputEvent::kMouseDown || type == WebInputEvent::kKeyDown ||
+ type == WebInputEvent::kRawKeyDown ||
+ // We need to explicitly include tap, as if there are no listeners, we
+ // won't receive the pointer events.
+ type == WebInputEvent::kGestureTap || is_hovering_pointerdown ||
+ pointerdown_became_tap;
+
+ if (should_trigger_shift_exclusion) {
+ observed_input_or_scroll_ = true;
+
+ // This cancels any previously scheduled task from the same timer.
+ timer_.StartOneShot(kTimerDelay, FROM_HERE);
+ UpdateInputTimestamp(event.TimeStamp());
+ }
+
+ if (saw_pointerdown && event_type_stops_pointerdown_buffering) {
+ double score_delta = pointerdown_pending_data_.score_delta;
+ if (score_delta > 0)
+ ReportShift(score_delta, pointerdown_pending_data_.weighted_score_delta);
+ pointerdown_pending_data_ = PointerdownPendingData();
+ }
+ if (type == WebInputEvent::kPointerDown && !is_hovering_pointerdown)
+ pointerdown_pending_data_.saw_pointerdown = true;
+}
+
+void LayoutShiftTracker::UpdateInputTimestamp(base::TimeTicks timestamp) {
+ if (!most_recent_input_timestamp_initialized_) {
+ most_recent_input_timestamp_ = timestamp;
+ most_recent_input_timestamp_initialized_ = true;
+ } else if (timestamp > most_recent_input_timestamp_) {
+ most_recent_input_timestamp_ = timestamp;
+ }
+}
+
+void LayoutShiftTracker::NotifyScroll(ScrollType scroll_type,
+ ScrollOffset delta) {
+ frame_scroll_delta_ += delta;
+
+ // Only set observed_input_or_scroll_ for user-initiated scrolls, and not
+ // other scrolls such as hash fragment navigations.
+ if (scroll_type == kUserScroll || scroll_type == kCompositorScroll)
+ observed_input_or_scroll_ = true;
+}
+
+void LayoutShiftTracker::NotifyViewportSizeChanged() {
+ // This cancels any previously scheduled task from the same timer.
+ timer_.StartOneShot(kTimerDelay, FROM_HERE);
+ UpdateInputTimestamp(base::TimeTicks::Now());
+}
+
+bool LayoutShiftTracker::IsActive() {
+ // This eliminates noise from the private Page object created by
+ // SVGImage::DataChanged.
+ if (frame_view_->GetFrame().GetChromeClient().IsSVGImageChromeClient())
+ return false;
+ return true;
+}
+
+std::unique_ptr<TracedValue> LayoutShiftTracker::PerFrameTraceData(
+ double score_delta,
+ bool input_detected) const {
+ auto value = std::make_unique<TracedValue>();
+ value->SetDouble("score", score_delta);
+ value->SetDouble("cumulative_score", score_);
+ value->SetDouble("overall_max_distance", overall_max_distance_);
+ value->SetDouble("frame_max_distance", frame_max_distance_);
+
+ float granularity_scale = RegionGranularityScale(
+ IntRect(IntPoint(),
+ frame_view_->GetScrollableArea()->VisibleContentRect().Size()));
+ if (RuntimeEnabledFeatures::JankTrackingSweepLineEnabled())
+ RegionToTracedValue(region_experimental_, granularity_scale, *value);
+ else
+ RegionToTracedValue(region_, granularity_scale, *value);
+
+ value->SetBoolean("is_main_frame", frame_view_->GetFrame().IsMainFrame());
+ value->SetBoolean("had_recent_input", input_detected);
+ return value;
+}
+
+WebVector<gfx::Rect> LayoutShiftTracker::ConvertIntRectsToGfxRects(
+ const Vector<IntRect>& int_rects,
+ double granularity_scale) {
+ WebVector<gfx::Rect> rects;
+ for (const IntRect& rect : int_rects) {
+ gfx::Rect r = gfx::Rect(
+ rect.X() / granularity_scale, rect.Y() / granularity_scale,
+ rect.Width() / granularity_scale, rect.Height() / granularity_scale);
+ rects.emplace_back(r);
+ }
+ return rects;
+}
+
+void LayoutShiftTracker::SetLayoutShiftRects(const Vector<IntRect>& int_rects,
+ double granularity_scale,
+ bool using_sweep_line) {
+ // Store the layout shift rects in the HUD layer.
+ GraphicsLayer* root_graphics_layer =
+ frame_view_->GetLayoutView()->Compositor()->RootGraphicsLayer();
+ if (!root_graphics_layer)
+ return;
+
+ cc::Layer* cc_layer = root_graphics_layer->CcLayer();
+ if (!cc_layer)
+ return;
+ if (cc_layer->layer_tree_host()) {
+ if (!cc_layer->layer_tree_host()->GetDebugState().show_layout_shift_regions)
+ return;
+ if (cc_layer->layer_tree_host()->hud_layer()) {
+ WebVector<gfx::Rect> rects;
+ if (using_sweep_line) {
+ Region old_region;
+ for (IntRect rect : int_rects)
+ old_region.Unite(Region(rect));
+ rects =
+ ConvertIntRectsToGfxRects(old_region.Rects(), granularity_scale);
+ } else {
+ rects = ConvertIntRectsToGfxRects(int_rects, granularity_scale);
+ }
+ cc_layer->layer_tree_host()->hud_layer()->SetLayoutShiftRects(
+ rects.ReleaseVector());
+ cc_layer->layer_tree_host()->hud_layer()->SetNeedsPushProperties();
+ }
+ }
+}
+
+} // namespace blink