Skip to content

Commit

Permalink
[LCP] Ignore paints with opacity 0
Browse files Browse the repository at this point in the history
This changes the opacity 0 paints that are ignored by the LCP algorithm.
Before, paints that would later be composited were not ignored, which
resulted in an inaccurate metric due to detecting LCP too early. After
this change, even will-change opacity paints are ignored, which could
result in elements not becoming candidates because they are never
repainted. In the special case where documentElement changes its
opacity, we consider the largest content that becomes visible as a valid
LCP candidate.

In other words this CL does the following 3 changes:
1) Ignores all LCP signals under style.opacity: 0, as before styles with
'will-change: opacity' were not ignored.
2) Saves off the LCP for ignored LCP signals if they are under
non-nested style.opacity = 0.
3.) If the document opacity style changes from opacity: 0 to non-zero,
we record the saved off LCP signal.

Bug: 1092473
Change-Id: I2a970a34fdb1db6c7c746060c286077553b816a6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2316788
Commit-Queue: Nicolás Peña Moreno <npm@chromium.org>
Reviewed-by: Xianzhu Wang <wangxianzhu@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#794689}
  • Loading branch information
npm1 authored and Commit Bot committed Aug 4, 2020
1 parent 1fda3cc commit 651f631
Show file tree
Hide file tree
Showing 17 changed files with 360 additions and 41 deletions.
5 changes: 5 additions & 0 deletions third_party/blink/renderer/core/layout/layout_object.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,11 @@ void LayoutObject::StyleWillChange(StyleDifference diff,
}
MarkEffectiveAllowedTouchActionChanged();
}
if (is_document_element && style_ && style_->Opacity() == 0.0f &&
new_style.Opacity() != 0.0f) {
if (LocalFrameView* frame_view = GetFrameView())
frame_view->GetPaintTimingDetector().ReportIgnoredContent();
}
}

void LayoutObject::ClearBaseComputedStyle() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
#include "third_party/blink/renderer/platform/graphics/paint/cull_rect.h"
#include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h"
#include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h"
#include "third_party/blink/renderer/platform/graphics/paint/ignore_paint_timing_scope.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_controller.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
Expand Down Expand Up @@ -1710,11 +1711,34 @@ void CompositedLayerMapping::DoPaintTask(
float device_scale_factor = blink::DeviceScaleFactorDeprecated(
paint_info.paint_layer->GetLayoutObject().GetFrame());
context.SetDeviceScaleFactor(device_scale_factor);

Settings* settings = GetLayoutObject().GetFrame()->GetSettings();
context.SetDarkMode(
BuildDarkModeSettings(*settings, *GetLayoutObject().View()));

// As a composited layer may be painted directly, we need to traverse the
// effect tree starting from the current node all the way up through the
// parents to determine which effects are opacity 0, for the purposes of
// correctly computing paint metrics such as First Contentful Paint and
// Largest Contentful Paint. For the latter we special-case the nodes where
// the opacity:0 depth is 1, so we need to only compute up to the first two
// opacity:0 effects in here and can ignore the rest.
base::Optional<IgnorePaintTimingScope> ignore_paint_timing_scope;
int num_ignores = 0;
DCHECK_EQ(IgnorePaintTimingScope::IgnoreDepth(), 0);
for (const auto* effect_node = &paint_info.paint_layer->GetLayoutObject()
.FirstFragment()
.PreEffect()
.Unalias();
effect_node && num_ignores < 2;
effect_node = effect_node->UnaliasedParent()) {
if (effect_node->Opacity() == 0.0f) {
if (!ignore_paint_timing_scope)
ignore_paint_timing_scope.emplace();
IgnorePaintTimingScope::IncrementIgnoreDepth();
++num_ignores;
}
}

if (paint_info.paint_layer->GetCompositingState() !=
kPaintsIntoGroupedBacking) {
// FIXME: GraphicsLayers need a way to split for multicol.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,23 @@ void ImagePaintTimingDetector::RecordImage(
return;

RecordId record_id = std::make_pair(&object, &cached_image);
bool is_recored_visible_image =
bool is_recorded_visible_image =
records_manager_.IsRecordedVisibleImage(record_id);
if (is_recored_visible_image &&
if (int depth = IgnorePaintTimingScope::IgnoreDepth()) {
// Record the largest loaded image that is hidden due to documentElement
// being invisible but by no other reason (i.e. IgnoreDepth() needs to be
// 1).
if (depth == 1 && IgnorePaintTimingScope::IsDocumentElementInvisible() &&
!is_recorded_visible_image && cached_image.IsLoaded()) {
uint64_t rect_size = ComputeImageRectSize(image_border, intrinsic_size,
current_paint_chunk_properties,
object, cached_image);
records_manager_.MaybeUpdateLargestIgnoredImage(record_id, rect_size);
}
return;
}

if (is_recorded_visible_image &&
!records_manager_.IsVisibleImageLoaded(record_id) &&
cached_image.IsLoaded()) {
records_manager_.OnImageLoaded(record_id, frame_index_, style_image);
Expand All @@ -244,13 +258,33 @@ void ImagePaintTimingDetector::RecordImage(
return;
}

if (is_recored_visible_image || !is_recording_)
if (is_recorded_visible_image || !is_recording_)
return;

// Before the image resource starts loading, <img> has no size info. We wait
// until the size is known.
if (image_border.IsEmpty())
return;
uint64_t rect_size = ComputeImageRectSize(image_border, intrinsic_size,
current_paint_chunk_properties,
object, cached_image);
if (rect_size == 0) {
records_manager_.RecordInvisible(object);
} else {
records_manager_.RecordVisible(record_id, rect_size);
if (cached_image.IsLoaded()) {
records_manager_.OnImageLoaded(record_id, frame_index_, style_image);
need_update_timing_at_frame_end_ = true;
}
}
}

uint64_t ImagePaintTimingDetector::ComputeImageRectSize(
const IntRect& image_border,
const IntSize& intrinsic_size,
const PropertyTreeStateOrAlias& current_paint_chunk_properties,
const LayoutObject& object,
const ImageResourceContent& cached_image) {
FloatRect mapped_visual_rect =
frame_view_->GetPaintTimingDetector().CalculateVisualRect(
image_border, current_paint_chunk_properties);
Expand All @@ -267,15 +301,7 @@ void ImagePaintTimingDetector::RecordImage(
rect_size = DownScaleIfIntrinsicSizeIsSmaller(
rect_size, intrinsic_size.Area(),
float_visual_rect.width * float_visual_rect.height);
if (rect_size == 0) {
records_manager_.RecordInvisible(object);
} else {
records_manager_.RecordVisible(record_id, rect_size);
if (cached_image.IsLoaded()) {
records_manager_.OnImageLoaded(record_id, frame_index_, style_image);
need_update_timing_at_frame_end_ = true;
}
}
return rect_size;
}

void ImagePaintTimingDetector::NotifyImageFinished(
Expand All @@ -285,6 +311,11 @@ void ImagePaintTimingDetector::NotifyImageFinished(
records_manager_.NotifyImageFinished(record_id);
}

void ImagePaintTimingDetector::ReportLargestIgnoredImage() {
need_update_timing_at_frame_end_ = true;
records_manager_.ReportLargestIgnoredImage(frame_index_);
}

ImageRecordsManager::ImageRecordsManager(LocalFrameView* frame_view)
: size_ordered_set_(&LargeImageFirst), frame_view_(frame_view) {}

Expand All @@ -306,13 +337,43 @@ void ImageRecordsManager::OnImageLoaded(const RecordId& record_id,
OnImageLoadedInternal(record, current_frame_index);
}

void ImageRecordsManager::ReportLargestIgnoredImage(
unsigned current_frame_index) {
if (!largest_ignored_image_)
return;
base::WeakPtr<ImageRecord> record = largest_ignored_image_->AsWeakPtr();
Node* node = DOMNodeIds::NodeForId(largest_ignored_image_->node_id);
if (!node || !node->GetLayoutObject() ||
!largest_ignored_image_->cached_image) {
// The image has been removed, so we have no content to report.
largest_ignored_image_.reset();
return;
}
RecordId record_id = std::make_pair(node->GetLayoutObject(),
largest_ignored_image_->cached_image);
size_ordered_set_.insert(record);
visible_images_.insert(record_id, std::move(largest_ignored_image_));
OnImageLoadedInternal(record, current_frame_index);
}

void ImageRecordsManager::OnImageLoadedInternal(
base::WeakPtr<ImageRecord>& record,
unsigned current_frame_index) {
SetLoaded(record);
QueueToMeasurePaintTime(record, current_frame_index);
}

void ImageRecordsManager::MaybeUpdateLargestIgnoredImage(
const RecordId& record_id,
const uint64_t& visual_size) {
if (visual_size && (!largest_ignored_image_ ||
visual_size > largest_ignored_image_->first_size)) {
largest_ignored_image_ =
CreateImageRecord(*record_id.first, record_id.second, visual_size);
largest_ignored_image_->load_time = base::TimeTicks::Now();
}
}

void ImageRecordsManager::RecordVisible(const RecordId& record_id,
const uint64_t& visual_size) {
std::unique_ptr<ImageRecord> record =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ class CORE_EXPORT ImageRecordsManager {
void OnImageLoadedInternal(base::WeakPtr<ImageRecord>&,
unsigned current_frame_index);

// Receives a candidate image painted under opacity 0 but without nested
// opacity. May update |largest_ignored_image_| if the new candidate has a
// larger size.
void MaybeUpdateLargestIgnoredImage(const RecordId&,
const uint64_t& visual_size);
void ReportLargestIgnoredImage(unsigned current_frame_index);

// Compare the last frame index in queue with the last frame index that has
// registered for assigning paint time.
inline bool HasUnregisteredRecordsInQueue(
Expand Down Expand Up @@ -202,6 +209,14 @@ class CORE_EXPORT ImageRecordsManager {
uint64_t largest_removed_image_size_ = 0u;
base::TimeTicks largest_removed_image_paint_time_;

// Image paints are ignored when they (or an ancestor) have opacity 0. This
// can be a problem later on if the opacity changes to nonzero but this change
// is composited. We solve this for the special case of documentElement by
// storing a record for the largest ignored image without nested opacity. We
// consider this an LCP candidate when the documentElement's opacity changes
// from zero to nonzero.
std::unique_ptr<ImageRecord> largest_ignored_image_;

DISALLOW_COPY_AND_ASSIGN(ImageRecordsManager);
};

Expand Down Expand Up @@ -263,6 +278,11 @@ class CORE_EXPORT ImagePaintTimingDetector final
// Return the candidate.
ImageRecord* UpdateCandidate();

// Called when documentElement changes from zero to nonzero opacity. Makes the
// largest image that was hidden due to this a Largest Contentful Paint
// candidate.
void ReportLargestIgnoredImage();

void Trace(Visitor*) const;

private:
Expand All @@ -272,6 +292,11 @@ class CORE_EXPORT ImagePaintTimingDetector final
void RegisterNotifySwapTime();
void ReportCandidateToTrace(ImageRecord&);
void ReportNoCandidateToTrace();
uint64_t ComputeImageRectSize(const IntRect&,
const IntSize&,
const PropertyTreeStateOrAlias&,
const LayoutObject&,
const ImageResourceContent&);

// Used to find the last candidate.
unsigned count_candidates_ = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1183,4 +1183,55 @@ TEST_P(ImagePaintTimingDetectorTest,
EXPECT_EQ(record->first_size, 25u);
}

TEST_P(ImagePaintTimingDetectorTest, OpacityZeroHTML) {
SetBodyInnerHTML(R"HTML(
<style>
:root {
opacity: 0;
will-change: opacity;
}
</style>
<img id="target"></img>
)HTML");
SetImageAndPaint("target", 5, 5);
UpdateAllLifecyclePhasesAndInvokeCallbackIfAny();
EXPECT_EQ(CountVisibleImageRecords(), 0u);

// Change the opacity of documentElement, now the img should be a candidate.
GetDocument().documentElement()->setAttribute(html_names::kStyleAttr,
"opacity: 1");
UpdateAllLifecyclePhasesAndInvokeCallbackIfAny();
EXPECT_EQ(CountVisibleImageRecords(), 1u);
EXPECT_EQ(GetPerformanceTiming().LargestImagePaintSize(), 25u);
EXPECT_GT(GetPerformanceTiming().LargestImagePaint(), 0u);
EXPECT_EQ(GetPerformanceTiming().LargestImagePaintSize(),
GetPerformanceTiming().ExperimentalLargestImagePaintSize());
EXPECT_EQ(GetPerformanceTiming().LargestImagePaint(),
GetPerformanceTiming().ExperimentalLargestImagePaint());
}

TEST_P(ImagePaintTimingDetectorTest, OpacityZeroHTML2) {
SetBodyInnerHTML(R"HTML(
<style>
#target {
opacity: 0;
}
</style>
<img id="target"></img>
)HTML");
SetImageAndPaint("target", 5, 5);
UpdateAllLifecyclePhasesAndInvokeCallbackIfAny();
EXPECT_EQ(CountVisibleImageRecords(), 0u);

GetDocument().documentElement()->setAttribute(html_names::kStyleAttr,
"opacity: 0");
UpdateAllLifecyclePhasesAndInvokeCallbackIfAny();
EXPECT_EQ(CountVisibleImageRecords(), 0u);

GetDocument().documentElement()->setAttribute(html_names::kStyleAttr,
"opacity: 1");
UpdateAllLifecyclePhasesAndInvokeCallbackIfAny();
EXPECT_EQ(CountVisibleImageRecords(), 0u);
}

} // namespace blink
19 changes: 16 additions & 3 deletions third_party/blink/renderer/core/paint/paint_layer_painter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,22 @@ PaintResult PaintLayerPainter::PaintLayerContents(
if (selection_drag_image_only && !paint_layer_.GetLayoutObject().IsSelected())
return result;

base::Optional<IgnorePaintTimingScope> ignore_paint_timing;
if (PaintedOutputInvisible(paint_layer_.GetLayoutObject().StyleRef()))
ignore_paint_timing.emplace();
IgnorePaintTimingScope ignore_paint_timing;
if (paint_layer_.GetLayoutObject().StyleRef().Opacity() == 0.0f) {
IgnorePaintTimingScope::IncrementIgnoreDepth();
}
// Explicitly compute opacity of documentElement, as it is special-cased in
// Largest Contentful Paint.
bool is_document_element_invisible = false;
if (const auto* document_element =
paint_layer_.GetLayoutObject().GetDocument().documentElement()) {
if (document_element->GetLayoutObject() &&
document_element->GetLayoutObject()->StyleRef().Opacity() == 0.0f) {
is_document_element_invisible = true;
}
}
IgnorePaintTimingScope::SetIsDocumentElementInvisible(
is_document_element_invisible);

PaintLayerFlags paint_flags = paint_flags_arg;
PaintLayerPaintingInfo painting_info = painting_info_arg;
Expand Down
18 changes: 10 additions & 8 deletions third_party/blink/renderer/core/paint/paint_timing_detector.cc
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ void PaintTimingDetector::NotifyBackgroundImagePaint(
const StyleFetchedImage* style_image,
const PropertyTreeStateOrAlias& current_paint_chunk_properties,
const IntRect& image_border) {
if (IgnorePaintTimingScope::ShouldIgnore())
return;

DCHECK(image);
DCHECK(style_image->CachedImage());
if (!node)
Expand Down Expand Up @@ -140,9 +137,6 @@ void PaintTimingDetector::NotifyImagePaint(
const ImageResourceContent* cached_image,
const PropertyTreeStateOrAlias& current_paint_chunk_properties,
const IntRect& image_border) {
if (IgnorePaintTimingScope::ShouldIgnore())
return;

LocalFrameView* frame_view = object.GetFrameView();
if (!frame_view)
return;
Expand Down Expand Up @@ -385,15 +379,23 @@ void PaintTimingDetector::UpdateLargestContentfulPaintCandidate() {
largest_image_record);
}

void PaintTimingDetector::ReportIgnoredContent() {
if (auto* text_timing_detector = GetTextPaintTimingDetector()) {
text_paint_timing_detector_->ReportLargestIgnoredText();
}
if (auto* image_timing_detector = GetImagePaintTimingDetector()) {
image_timing_detector->ReportLargestIgnoredImage();
}
}

ScopedPaintTimingDetectorBlockPaintHook*
ScopedPaintTimingDetectorBlockPaintHook::top_ = nullptr;

void ScopedPaintTimingDetectorBlockPaintHook::EmplaceIfNeeded(
const LayoutBoxModelObject& aggregator,
const PropertyTreeStateOrAlias& property_tree_state) {
if (IgnorePaintTimingScope::ShouldIgnore())
if (IgnorePaintTimingScope::IgnoreDepth() > 1)
return;

// |reset_top_| is unset when |aggregator| is anonymous so that each
// aggregation corresponds to an element. See crbug.com/988593. When set,
// |top_| becomes |this|, and |top_| is restored to the previous value when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ class CORE_EXPORT PaintTimingDetector

void UpdateLargestContentfulPaintCandidate();

// Reports the largest image and text candidates painted under non-nested 0
// opacity layer.
void ReportIgnoredContent();

base::Optional<PaintTimingVisualizer>& Visualizer() { return visualizer_; }
void Trace(Visitor* visitor) const;

Expand Down Expand Up @@ -308,7 +312,7 @@ class ScopedPaintTimingDetectorBlockPaintHook {
// static
inline void PaintTimingDetector::NotifyTextPaint(
const IntRect& text_visual_rect) {
if (IgnorePaintTimingScope::ShouldIgnore())
if (IgnorePaintTimingScope::IgnoreDepth() > 1)
return;
ScopedPaintTimingDetectorBlockPaintHook::AggregateTextPaint(text_visual_rect);
}
Expand Down
Loading

0 comments on commit 651f631

Please sign in to comment.