blob: 5d62696a1c0fba0a680c0eb127940f7b117d4b8a [file] [log] [blame]
// 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/paint/image_paint_timing_detector.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/inspector/identifiers_factory.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/layout/layout_image.h"
#include "third_party/blink/renderer/core/layout/layout_image_resource.h"
#include "third_party/blink/renderer/core/layout/layout_video.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/layout/svg/layout_svg_image.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/paint/paint_layer.h"
#include "third_party/blink/renderer/core/paint/paint_timing_detector.h"
#include "third_party/blink/renderer/core/style/style_fetched_image.h"
#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
#include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/traced_value.h"
namespace blink {
namespace {
String GetImageUrl(const LayoutObject& object) {
if (object.IsImage()) {
const ImageResourceContent* cached_image =
ToLayoutImage(&object)->CachedImage();
return cached_image ? cached_image->Url().StrippedForUseAsReferrer() : "";
}
if (object.IsVideo()) {
const ImageResourceContent* cached_image =
ToLayoutVideo(&object)->CachedImage();
return cached_image ? cached_image->Url().StrippedForUseAsReferrer() : "";
}
if (object.IsSVGImage()) {
const LayoutImageResource* image_resource =
ToLayoutSVGImage(&object)->ImageResource();
const ImageResourceContent* cached_image = image_resource->CachedImage();
return cached_image ? cached_image->Url().StrippedForUseAsReferrer() : "";
}
DCHECK(ImagePaintTimingDetector::HasContentfulBackgroundImage(object));
const ComputedStyle* style = object.Style();
StringBuilder concatenated_result;
for (const FillLayer* bg_layer = &style->BackgroundLayers(); bg_layer;
bg_layer = bg_layer->Next()) {
StyleImage* bg_image = bg_layer->GetImage();
if (!bg_image || !bg_image->IsImageResource())
continue;
const StyleFetchedImage* fetched_image = ToStyleFetchedImage(bg_image);
const String url = fetched_image->Url().StrippedForUseAsReferrer();
concatenated_result.Append(url.Utf8().data(), url.length());
}
return concatenated_result.ToString();
}
bool AttachedBackgroundImagesAllLoaded(const LayoutObject& object) {
DCHECK(ImagePaintTimingDetector::HasContentfulBackgroundImage(object));
const ComputedStyle* style = object.Style();
DCHECK(style);
for (const FillLayer* bg_layer = &style->BackgroundLayers(); bg_layer;
bg_layer = bg_layer->Next()) {
StyleImage* bg_image = bg_layer->GetImage();
// A layout object with background images is not loaded until all of the
// background images are loaded.
if (!bg_image || !bg_image->IsImageResource())
continue;
if (!bg_image->IsLoaded())
return false;
}
return true;
}
bool IsLoaded(const LayoutObject& object) {
if (object.IsImage()) {
const ImageResourceContent* cached_image =
ToLayoutImage(&object)->CachedImage();
return cached_image ? cached_image->IsLoaded() : false;
}
if (object.IsVideo()) {
const ImageResourceContent* cached_image =
ToLayoutVideo(&object)->CachedImage();
return cached_image ? cached_image->IsLoaded() : false;
}
if (object.IsSVGImage()) {
const LayoutImageResource* image_resource =
ToLayoutSVGImage(&object)->ImageResource();
const ImageResourceContent* cached_image = image_resource->CachedImage();
return cached_image ? cached_image->IsLoaded() : false;
}
DCHECK(ImagePaintTimingDetector::HasContentfulBackgroundImage(object));
return AttachedBackgroundImagesAllLoaded(object);
}
} // namespace
// Set a big enough limit for the number of nodes to ensure memory usage is
// capped. Exceeding such limit will deactivate the algorithm.
constexpr size_t kImageNodeNumberLimit = 5000;
static bool LargeImageOnTop(const base::WeakPtr<ImageRecord>& a,
const base::WeakPtr<ImageRecord>& b) {
// null value should be at the bottom of the priority queue, so:
// * When a == null && b!=null, we treat b as larger, return true.
// * When a != null && b==null, we treat b as no larger than a, return false.
// * When a == null && b==null, we treat b as no larger than a, return false.
return (a && b) ? (a->first_size < b->first_size) : bool(b);
}
static bool LateImageOnTop(const base::WeakPtr<ImageRecord>& a,
const base::WeakPtr<ImageRecord>& b) {
// null value should be at the bottom of the priority queue, so:
// * When a == null && b!=null, we treat b as larger, return true.
// * When a != null && b==null, we treat b as no larger than a, return false.
// * When a == null && b==null, we treat b as no larger than a, return false.
return (a && b) ? (a->first_paint_index < b->first_paint_index) : bool(b);
}
ImagePaintTimingDetector::ImagePaintTimingDetector(LocalFrameView* frame_view)
: largest_image_heap_(&LargeImageOnTop),
latest_image_heap_(&LateImageOnTop),
frame_view_(frame_view) {}
void ImagePaintTimingDetector::PopulateTraceValue(
TracedValue& value,
const ImageRecord& first_image_paint,
unsigned candidate_index) const {
value.SetInteger("DOMNodeId", first_image_paint.node_id);
value.SetString("imageUrl", first_image_paint.image_url);
value.SetInteger("size", first_image_paint.first_size);
value.SetInteger("candidateIndex", candidate_index);
value.SetString("frame",
IdentifiersFactory::FrameId(&frame_view_->GetFrame()));
}
IntRect ImagePaintTimingDetector::CalculateTransformedRect(
LayoutRect& invalidated_rect,
const PaintLayer& painting_layer) const {
const auto* local_transform = painting_layer.GetLayoutObject()
.FirstFragment()
.LocalBorderBoxProperties()
.Transform();
const auto* ancestor_transform = painting_layer.GetLayoutObject()
.View()
->FirstFragment()
.LocalBorderBoxProperties()
.Transform();
FloatRect invalidated_rect_abs = FloatRect(invalidated_rect);
if (invalidated_rect_abs.IsEmpty() || invalidated_rect_abs.IsZero())
return IntRect();
DCHECK(local_transform);
DCHECK(ancestor_transform);
GeometryMapper::SourceToDestinationRect(local_transform, ancestor_transform,
invalidated_rect_abs);
IntRect invalidated_rect_in_viewport = RoundedIntRect(invalidated_rect_abs);
ScrollableArea* scrollable_area = frame_view_->GetScrollableArea();
DCHECK(scrollable_area);
IntRect viewport = scrollable_area->VisibleContentRect();
invalidated_rect_in_viewport.Intersect(viewport);
return invalidated_rect_in_viewport;
}
void ImagePaintTimingDetector::OnLargestImagePaintDetected(
const ImageRecord& largest_image_record) {
largest_image_paint_ = largest_image_record.first_paint_time_after_loaded;
std::unique_ptr<TracedValue> value = TracedValue::Create();
PopulateTraceValue(*value, largest_image_record,
++largest_image_candidate_index_max_);
TRACE_EVENT_INSTANT_WITH_TIMESTAMP1(
"loading", "LargestImagePaint::Candidate", TRACE_EVENT_SCOPE_THREAD,
largest_image_record.first_paint_time_after_loaded, "data",
std::move(value));
frame_view_->GetPaintTimingDetector().DidChangePerformanceTiming();
}
void ImagePaintTimingDetector::OnLastImagePaintDetected(
const ImageRecord& last_image_record) {
last_image_paint_ = last_image_record.first_paint_time_after_loaded;
std::unique_ptr<TracedValue> value = TracedValue::Create();
PopulateTraceValue(*value, last_image_record,
++last_image_candidate_index_max_);
TRACE_EVENT_INSTANT_WITH_TIMESTAMP1(
"loading", "LastImagePaint::Candidate", TRACE_EVENT_SCOPE_THREAD,
last_image_record.first_paint_time_after_loaded, "data",
std::move(value));
frame_view_->GetPaintTimingDetector().DidChangePerformanceTiming();
}
void ImagePaintTimingDetector::Analyze() {
// These conditions represents the following scenarios:
// 1. candiate being nullptr: no loaded image is found.
// 2. candidate's first paint being null: largest/last image is still pending
// for timing. We discard the candidate and wait for the next analysis.
// 3. new candidate equals to old candidate: we don't need to update the
// result unless it's a new candidate.
ImageRecord* largest_image_record = FindLargestPaintCandidate();
bool new_candidate_detected = false;
if (largest_image_record &&
!largest_image_record->first_paint_time_after_loaded.is_null() &&
largest_image_record->first_paint_time_after_loaded !=
largest_image_paint_) {
new_candidate_detected = true;
OnLargestImagePaintDetected(*largest_image_record);
}
ImageRecord* last_image_record = FindLastPaintCandidate();
if (last_image_record &&
!last_image_record->first_paint_time_after_loaded.is_null() &&
last_image_record->first_paint_time_after_loaded != last_image_paint_) {
new_candidate_detected = true;
OnLastImagePaintDetected(*last_image_record);
}
if (new_candidate_detected) {
frame_view_->GetPaintTimingDetector().DidChangePerformanceTiming();
}
}
void ImagePaintTimingDetector::OnPrePaintFinished() {
frame_index_++;
if (records_pending_timing_.size() <= 0)
return;
// If the last frame index of queue has changed, it means there are new
// records pending timing.
DOMNodeId node_id = records_pending_timing_.back();
if (!id_record_map_.Contains(node_id))
return;
unsigned last_frame_index = id_record_map_.at(node_id)->frame_index;
if (last_frame_index_queued_for_timing_ >= last_frame_index)
return;
last_frame_index_queued_for_timing_ = last_frame_index;
// Records with frame index up to last_frame_index_queued_for_timing_ will be
// queued for swap time.
RegisterNotifySwapTime();
}
void ImagePaintTimingDetector::NotifyNodeRemoved(DOMNodeId node_id) {
if (id_record_map_.Contains(node_id)) {
// We assume that the removed node's id wouldn't be recycled, so we don't
// bother to remove these records from largest_image_heap_ or
// latest_image_heap_, to reduce computation.
id_record_map_.erase(node_id);
if (id_record_map_.size() == 0) {
const bool largest_image_paint_invalidated =
largest_image_paint_ != base::TimeTicks();
const bool last_image_paint_invalidated =
last_image_paint_ != base::TimeTicks();
if (largest_image_paint_invalidated)
largest_image_paint_ = base::TimeTicks();
if (last_image_paint_invalidated)
last_image_paint_ = base::TimeTicks();
if (largest_image_paint_invalidated || last_image_paint_invalidated) {
frame_view_->GetPaintTimingDetector().DidChangePerformanceTiming();
}
}
}
}
void ImagePaintTimingDetector::RegisterNotifySwapTime() {
WebLayerTreeView::ReportTimeCallback callback =
WTF::Bind(&ImagePaintTimingDetector::ReportSwapTime,
WrapWeakPersistent(this), last_frame_index_queued_for_timing_);
if (notify_swap_time_override_for_testing_) {
// Run is not to run the |callback|, but to queue it.
notify_swap_time_override_for_testing_.Run(std::move(callback));
return;
}
// ReportSwapTime on layerTreeView will queue a swap-promise, the callback is
// called when the swap for current render frame completes or fails to happen.
LocalFrame& frame = frame_view_->GetFrame();
if (!frame.GetPage())
return;
WebLayerTreeView* layerTreeView =
frame.GetPage()->GetChromeClient().GetWebLayerTreeView(&frame);
if (!layerTreeView)
return;
layerTreeView->NotifySwapTime(std::move(callback));
}
void ImagePaintTimingDetector::ReportSwapTime(
unsigned max_frame_index_to_time,
WebLayerTreeView::SwapResult result,
base::TimeTicks timestamp) {
// The callback is safe from race-condition only when running on main-thread.
DCHECK(ThreadState::Current()->IsMainThread());
// Not guranteed to be non-empty, because records can be removed between
// callback registration and invocation.
while (records_pending_timing_.size() > 0) {
DOMNodeId node_id = records_pending_timing_.front();
if (!id_record_map_.Contains(node_id)) {
records_pending_timing_.pop();
continue;
}
ImageRecord* record = id_record_map_.at(node_id);
if (record->frame_index > max_frame_index_to_time)
break;
record->first_paint_time_after_loaded = timestamp;
records_pending_timing_.pop();
}
Analyze();
}
// In the context of FCP++, we define contentful background image as one that
// satisfies all of the following conditions:
// * has image reources attached to style of the object, i.e.,
// { background-image: url('example.gif') }
// * not attached to <body> or <html>
//
// static
bool ImagePaintTimingDetector::HasContentfulBackgroundImage(
const LayoutObject& object) {
Node* node = object.GetNode();
if (!node)
return false;
// Background images attached to <body> or <html> are likely for background
// purpose, so we rule them out, according to the following rules:
// * Box model objects includes objects of box model, such as <div>, <body>,
// LayoutView, but not includes LayoutText.
// * BackgroundTransfersToView is true for the <body>, <html>, e.g., that
// have transferred their background to LayoutView.
// * LayoutView has the background transfered by <html> if <html> has
// background.
if (!object.IsBoxModelObject() ||
ToLayoutBoxModelObject(object).BackgroundTransfersToView())
return false;
if (object.IsLayoutView())
return false;
const ComputedStyle* style = object.Style();
if (!style)
return false;
if (!style->HasBackgroundImage())
return false;
for (const FillLayer* bg_layer = &style->BackgroundLayers(); bg_layer;
bg_layer = bg_layer->Next()) {
StyleImage* bg_image = bg_layer->GetImage();
// Rule out images that doesn't load any image resources, e.g., a gradient.
if (!bg_image || !bg_image->IsImageResource())
continue;
return true;
}
return false;
}
void ImagePaintTimingDetector::RecordImage(const LayoutObject& object,
const PaintLayer& painting_layer) {
Node* node = object.GetNode();
if (!node)
return;
DOMNodeId node_id = DOMNodeIds::IdForNode(node);
if (size_zero_ids_.Contains(node_id))
return;
if (!id_record_map_.Contains(node_id)) {
recorded_node_count_++;
if (recorded_node_count_ < kImageNodeNumberLimit) {
LayoutRect invalidated_rect = object.FirstFragment().VisualRect();
// Do not record first size until invalidated_rect's size becomes
// non-empty.
if (invalidated_rect.IsEmpty())
return;
IntRect invalidated_rect_in_viewport =
CalculateTransformedRect(invalidated_rect, painting_layer);
int rect_size = invalidated_rect_in_viewport.Height() *
invalidated_rect_in_viewport.Width();
if (rect_size == 0) {
// When rect_size == 0, it either means the image is size 0 or the image
// is out of viewport. Either way, we don't track this image anymore, to
// reduce computation.
size_zero_ids_.insert(node_id);
return;
}
std::unique_ptr<ImageRecord> record = std::make_unique<ImageRecord>();
record->node_id = node_id;
record->image_url = GetImageUrl(object);
// Mind that first_size has to be assigned at the push of
// largest_image_heap_ since it's the sorting key.
record->first_size = rect_size;
largest_image_heap_.push(record->AsWeakPtr());
id_record_map_.insert(node_id, std::move(record));
} else {
// for assessing whether kImageNodeNumberLimit is large enough for all
// normal cases
TRACE_EVENT_INSTANT1("loading", "ImagePaintTimingDetector::OverNodeLimit",
TRACE_EVENT_SCOPE_THREAD, "recorded_node_count",
recorded_node_count_);
}
}
if (id_record_map_.Contains(node_id) && !id_record_map_.at(node_id)->loaded &&
IsLoaded(object)) {
records_pending_timing_.push(node_id);
ImageRecord* record = id_record_map_.at(node_id);
record->frame_index = frame_index_;
record->loaded = true;
// Latest image heap differs from largest image heap in that the former
// pushes a record when an image is loaded while the latter pushes when an
// image is attached to DOM. This causes last image paint to base its order
// on load time other than attachment time.
// Mind that first_paint_index has to be assigned at the push of
// latest_image_heap_ since it's the sorting key.
record->first_paint_index = ++first_paint_index_max_;
latest_image_heap_.push(record->AsWeakPtr());
}
}
ImageRecord* ImagePaintTimingDetector::FindLargestPaintCandidate() {
while (!largest_image_heap_.empty() && !largest_image_heap_.top()) {
// Discard the elements that have been removed from |id_record_map_|.
largest_image_heap_.pop();
}
// We report the result as the first paint after the largest image finishes
// loading. If the largest image is still loading, we report nothing and come
// back later to see if the largest image by then has finished loading.
if (!largest_image_heap_.empty() && largest_image_heap_.top()->loaded) {
DCHECK(id_record_map_.Contains(largest_image_heap_.top()->node_id));
return largest_image_heap_.top().get();
}
return nullptr;
}
ImageRecord* ImagePaintTimingDetector::FindLastPaintCandidate() {
while (!latest_image_heap_.empty() && !latest_image_heap_.top()) {
// Discard the elements that have been removed from |id_record_map_|.
latest_image_heap_.pop();
}
// We report the result as the first paint after the latest image finishes
// loading. If the latest image is still loading, we report nothing and come
// back later to see if the latest image at that time has finished loading.
if (!latest_image_heap_.empty() && latest_image_heap_.top()->loaded) {
DCHECK(id_record_map_.Contains(latest_image_heap_.top()->node_id));
return latest_image_heap_.top().get();
}
return nullptr;
}
void ImagePaintTimingDetector::Trace(blink::Visitor* visitor) {
visitor->Trace(frame_view_);
}
} // namespace blink