blob: 45f5dc6be8d9743bf4998e97fa5829fcf7c6b4d1 [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 "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.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/layout/layout_image_resource.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/image_element_timing.h"
#include "third_party/blink/renderer/core/paint/largest_contentful_paint_calculator.h"
#include "third_party/blink/renderer/core/paint/paint_timing_detector.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 {
// In order for |rect_size| to align with the importance of the image, we
// use this heuristics to alleviate the effect of scaling. For example,
// an image has intrinsic size being 1x1 and scaled to 100x100, but only 50x100
// is visible in the viewport. In this case, |intrinsic_image_size| is 1x1;
// |displayed_image_size| is 100x100. |intrinsic_image_size| is 50x100.
// As the image do not have a lot of content, we down scale |visual_size| by the
// ratio of |intrinsic_image_size|/|displayed_image_size| = 1/10000.
//
// * |visual_size| refers to the size of the |displayed_image_size| after
// clipping and transforming. The size is in the main-frame's coordinate.
// * |intrinsic_image_size| refers to the the image object's original size
// before scaling. The size is in the image object's coordinate.
// * |displayed_image_size| refers to the paint size in the image object's
// coordinate.
uint64_t DownScaleIfIntrinsicSizeIsSmaller(
uint64_t visual_size,
const uint64_t& intrinsic_image_size,
const uint64_t& displayed_image_size) {
// This is an optimized equivalence to:
// |visual_size| * min(|displayed_image_size|, |intrinsic_image_size|) /
// |displayed_image_size|
if (intrinsic_image_size < displayed_image_size) {
DCHECK_GT(displayed_image_size, 0u);
return static_cast<double>(visual_size) * intrinsic_image_size /
displayed_image_size;
}
return visual_size;
}
} // namespace
static bool LargeImageFirst(const base::WeakPtr<ImageRecord>& a,
const base::WeakPtr<ImageRecord>& b) {
DCHECK(a);
DCHECK(b);
if (a->first_size != b->first_size)
return a->first_size > b->first_size;
// This make sure that two different |ImageRecord|s with the same |first_size|
// wouldn't be merged in the |size_ordered_set_|.
return a->insertion_index < b->insertion_index;
}
ImagePaintTimingDetector::ImagePaintTimingDetector(
LocalFrameView* frame_view,
PaintTimingCallbackManager* callback_manager)
: records_manager_(frame_view),
frame_view_(frame_view),
callback_manager_(callback_manager) {}
void ImagePaintTimingDetector::PopulateTraceValue(
TracedValue& value,
const ImageRecord& first_image_paint) {
value.SetInteger("DOMNodeId", static_cast<int>(first_image_paint.node_id));
// The cached_image could have been deleted when this is called.
value.SetString("imageUrl",
first_image_paint.cached_image
? String(first_image_paint.cached_image->Url())
: "(deleted)");
value.SetInteger("size", static_cast<int>(first_image_paint.first_size));
value.SetInteger("candidateIndex", ++count_candidates_);
value.SetBoolean("isMainFrame", frame_view_->GetFrame().IsMainFrame());
value.SetBoolean("isOOPIF",
!frame_view_->GetFrame().LocalFrameRoot().IsMainFrame());
if (first_image_paint.lcp_rect_info_) {
first_image_paint.lcp_rect_info_->OutputToTraceValue(value);
}
}
void ImagePaintTimingDetector::ReportCandidateToTrace(
ImageRecord& largest_image_record) {
if (!PaintTimingDetector::IsTracing())
return;
DCHECK(!largest_image_record.paint_time.is_null());
auto value = std::make_unique<TracedValue>();
PopulateTraceValue(*value, largest_image_record);
TRACE_EVENT_MARK_WITH_TIMESTAMP2("loading", "LargestImagePaint::Candidate",
largest_image_record.paint_time, "data",
std::move(value), "frame",
ToTraceValue(&frame_view_->GetFrame()));
}
void ImagePaintTimingDetector::ReportNoCandidateToTrace() {
if (!PaintTimingDetector::IsTracing())
return;
auto value = std::make_unique<TracedValue>();
value->SetInteger("candidateIndex", ++count_candidates_);
value->SetBoolean("isMainFrame", frame_view_->GetFrame().IsMainFrame());
value->SetBoolean("isOOPIF",
!frame_view_->GetFrame().LocalFrameRoot().IsMainFrame());
TRACE_EVENT2("loading", "LargestImagePaint::NoCandidate", "data",
std::move(value), "frame",
ToTraceValue(&frame_view_->GetFrame()));
}
ImageRecord* ImagePaintTimingDetector::UpdateCandidate() {
ImageRecord* largest_image_record =
records_manager_.FindLargestPaintCandidate();
const base::TimeTicks time = largest_image_record
? largest_image_record->paint_time
: base::TimeTicks();
const uint64_t size =
largest_image_record ? largest_image_record->first_size : 0;
PaintTimingDetector& detector = frame_view_->GetPaintTimingDetector();
// Two different candidates are rare to have the same time and size.
// So when they are unchanged, the candidate is considered unchanged.
bool changed = detector.NotifyIfChangedLargestImagePaint(
time, size, records_manager_.LargestRemovedImagePaintTime(),
records_manager_.LargestRemovedImageSize());
if (changed) {
if (!time.is_null()) {
DCHECK(largest_image_record->loaded);
ReportCandidateToTrace(*largest_image_record);
} else {
ReportNoCandidateToTrace();
}
}
return largest_image_record;
}
void ImagePaintTimingDetector::OnPaintFinished() {
frame_index_++;
viewport_size_ = base::nullopt;
if (need_update_timing_at_frame_end_) {
need_update_timing_at_frame_end_ = false;
frame_view_->GetPaintTimingDetector()
.UpdateLargestContentfulPaintCandidate();
}
if (!records_manager_.HasUnregisteredRecordsInQueue(
last_registered_frame_index_))
return;
last_registered_frame_index_ = frame_index_ - 1;
RegisterNotifyPresentationTime();
}
void ImagePaintTimingDetector::NotifyImageRemoved(
const LayoutObject& object,
const ImageResourceContent* cached_image) {
if (!is_recording_)
return;
RecordId record_id = std::make_pair(&object, cached_image);
records_manager_.RemoveImageFinishedRecord(record_id);
records_manager_.RemoveInvisibleRecordIfNeeded(record_id);
if (!records_manager_.IsRecordedVisibleImage(record_id))
return;
records_manager_.RemoveVisibleRecord(record_id);
need_update_timing_at_frame_end_ = true;
}
void ImagePaintTimingDetector::StopRecordEntries() {
is_recording_ = false;
if (frame_view_->GetFrame().IsMainFrame()) {
DCHECK(frame_view_->GetFrame().GetDocument());
ukm::builders::Blink_PaintTiming(
frame_view_->GetFrame().GetDocument()->UkmSourceID())
.SetLCPDebugging_HasViewportImage(contains_full_viewport_image_)
.Record(ukm::UkmRecorder::Get());
}
}
void ImagePaintTimingDetector::RegisterNotifyPresentationTime() {
auto callback = WTF::Bind(&ImagePaintTimingDetector::ReportPresentationTime,
WrapCrossThreadWeakPersistent(this),
last_registered_frame_index_);
callback_manager_->RegisterCallback(std::move(callback));
num_pending_presentation_callbacks_++;
}
void ImagePaintTimingDetector::ReportPresentationTime(
unsigned last_queued_frame_index,
base::TimeTicks timestamp) {
if (!is_recording_)
return;
// The callback is safe from race-condition only when running on main-thread.
DCHECK(ThreadState::Current()->IsMainThread());
records_manager_.AssignPaintTimeToRegisteredQueuedRecords(
timestamp, last_queued_frame_index);
num_pending_presentation_callbacks_--;
DCHECK_GE(num_pending_presentation_callbacks_, 0);
}
void ImageRecordsManager::AssignPaintTimeToRegisteredQueuedRecords(
const base::TimeTicks& timestamp,
unsigned last_queued_frame_index) {
// TODO(crbug.com/971419): should guarantee the queue not empty.
while (!images_queued_for_paint_time_.IsEmpty()) {
base::WeakPtr<ImageRecord>& record = images_queued_for_paint_time_.front();
if (!record) {
images_queued_for_paint_time_.pop_front();
continue;
}
if (record->frame_index > last_queued_frame_index)
break;
record->paint_time = timestamp;
images_queued_for_paint_time_.pop_front();
}
}
void ImagePaintTimingDetector::RecordImage(
const LayoutObject& object,
const IntSize& intrinsic_size,
const ImageResourceContent& cached_image,
const PropertyTreeStateOrAlias& current_paint_chunk_properties,
const StyleFetchedImage* style_image,
const IntRect& image_border) {
Node* node = object.GetNode();
if (!node)
return;
RecordId record_id = std::make_pair(&object, &cached_image);
if (records_manager_.IsRecordedInvisibleImage(record_id))
return;
bool is_recorded_visible_image =
records_manager_.IsRecordedVisibleImage(record_id);
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()) {
FloatRect mapped_visual_rect =
frame_view_->GetPaintTimingDetector().CalculateVisualRect(
image_border, current_paint_chunk_properties);
uint64_t rect_size = ComputeImageRectSize(
image_border, mapped_visual_rect, intrinsic_size,
current_paint_chunk_properties, object, cached_image);
records_manager_.MaybeUpdateLargestIgnoredImage(
record_id, rect_size, image_border, mapped_visual_rect);
}
return;
}
if (is_recorded_visible_image &&
!records_manager_.IsVisibleImageLoaded(record_id) &&
cached_image.IsLoaded()) {
records_manager_.OnImageLoaded(record_id, frame_index_, style_image);
need_update_timing_at_frame_end_ = true;
if (base::Optional<PaintTimingVisualizer>& visualizer =
frame_view_->GetPaintTimingDetector().Visualizer()) {
FloatRect mapped_visual_rect =
frame_view_->GetPaintTimingDetector().CalculateVisualRect(
image_border, current_paint_chunk_properties);
visualizer->DumpImageDebuggingRect(object, mapped_visual_rect,
cached_image);
}
return;
}
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;
FloatRect mapped_visual_rect =
frame_view_->GetPaintTimingDetector().CalculateVisualRect(
image_border, current_paint_chunk_properties);
uint64_t rect_size = ComputeImageRectSize(
image_border, mapped_visual_rect, intrinsic_size,
current_paint_chunk_properties, object, cached_image);
if (rect_size == 0) {
records_manager_.RecordInvisible(record_id);
} else {
records_manager_.RecordVisible(record_id, rect_size, image_border,
mapped_visual_rect);
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 FloatRect& mapped_visual_rect,
const IntSize& intrinsic_size,
const PropertyTreeStateOrAlias& current_paint_chunk_properties,
const LayoutObject& object,
const ImageResourceContent& cached_image) {
if (base::Optional<PaintTimingVisualizer>& visualizer =
frame_view_->GetPaintTimingDetector().Visualizer()) {
visualizer->DumpImageDebuggingRect(object, mapped_visual_rect,
cached_image);
}
uint64_t rect_size = mapped_visual_rect.Size().Area();
// Transform visual rect to window before calling downscale.
FloatRect float_visual_rect =
frame_view_->GetPaintTimingDetector().BlinkSpaceToDIPs(
FloatRect(image_border));
if (!viewport_size_.has_value()) {
FloatRect viewport = frame_view_->GetPaintTimingDetector().BlinkSpaceToDIPs(
FloatRect(frame_view_->GetScrollableArea()->VisibleContentRect()));
viewport_size_ = viewport.Size().Area();
}
// An SVG image size is computed with respect to the virtual viewport of the
// SVG, so |rect_size| can be larger than |*viewport_size| in edge cases. If
// the rect occupies the whole viewport, disregard this candidate by saying
// the size is 0.
if (rect_size >= *viewport_size_) {
contains_full_viewport_image_ = true;
return 0;
}
rect_size = DownScaleIfIntrinsicSizeIsSmaller(
rect_size, intrinsic_size.Area(),
float_visual_rect.Width() * float_visual_rect.Height());
return rect_size;
}
void ImagePaintTimingDetector::NotifyImageFinished(
const LayoutObject& object,
const ImageResourceContent* cached_image) {
RecordId record_id = std::make_pair(&object, cached_image);
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) {}
void ImageRecordsManager::OnImageLoaded(const RecordId& record_id,
unsigned current_frame_index,
const StyleFetchedImage* style_image) {
base::WeakPtr<ImageRecord> record = FindVisibleRecord(record_id);
DCHECK(record);
if (!style_image) {
record->load_time = image_finished_times_.at(record_id);
DCHECK(!record->load_time.is_null());
} else {
Document* document = frame_view_->GetFrame().GetDocument();
if (document && document->domWindow()) {
record->load_time = ImageElementTiming::From(*document->domWindow())
.GetBackgroundImageLoadTime(style_image);
}
}
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,
const IntRect& frame_visual_rect,
const FloatRect& root_visual_rect) {
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,
frame_visual_rect, root_visual_rect);
largest_ignored_image_->load_time = base::TimeTicks::Now();
}
}
void ImageRecordsManager::RecordVisible(const RecordId& record_id,
const uint64_t& visual_size,
const IntRect& frame_visual_rect,
const FloatRect& root_visual_rect) {
std::unique_ptr<ImageRecord> record =
CreateImageRecord(*record_id.first, record_id.second, visual_size,
frame_visual_rect, root_visual_rect);
size_ordered_set_.insert(record->AsWeakPtr());
visible_images_.insert(record_id, std::move(record));
}
std::unique_ptr<ImageRecord> ImageRecordsManager::CreateImageRecord(
const LayoutObject& object,
const ImageResourceContent* cached_image,
const uint64_t& visual_size,
const IntRect& frame_visual_rect,
const FloatRect& root_visual_rect) {
DCHECK_GT(visual_size, 0u);
Node* node = object.GetNode();
DOMNodeId node_id = DOMNodeIds::IdForNode(node);
std::unique_ptr<ImageRecord> record = std::make_unique<ImageRecord>(
node_id, cached_image, visual_size, frame_visual_rect, root_visual_rect);
return record;
}
ImageRecord* ImageRecordsManager::FindLargestPaintCandidate() const {
DCHECK_EQ(visible_images_.size(), size_ordered_set_.size());
if (size_ordered_set_.size() == 0)
return nullptr;
return size_ordered_set_.begin()->get();
}
void ImageRecordsManager::Trace(Visitor* visitor) const {
visitor->Trace(frame_view_);
}
void ImagePaintTimingDetector::Trace(Visitor* visitor) const {
visitor->Trace(records_manager_);
visitor->Trace(frame_view_);
visitor->Trace(callback_manager_);
}
} // namespace blink