blob: 39e0cd41f2be31a3949445c589c371848f2634bf [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.
// 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_IMAGE_PAINT_TIMING_DETECTOR_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_IMAGE_PAINT_TIMING_DETECTOR_H_
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/loader/resource/image_resource_content.h"
#include "third_party/blink/renderer/core/paint/paint_timing_detector.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
namespace blink {
class LayoutObject;
class LocalFrameView;
class PropertyTreeStateOrAlias;
class TracedValue;
class Image;
// TODO(crbug/960502): we should limit the access of these properties.
class ImageRecord : public base::SupportsWeakPtr<ImageRecord> {
public:
ImageRecord(DOMNodeId new_node_id,
const ImageResourceContent* new_cached_image,
uint64_t new_first_size,
const IntRect& frame_visual_rect,
const FloatRect& root_visual_rect)
: node_id(new_node_id),
cached_image(new_cached_image),
first_size(new_first_size) {
static unsigned next_insertion_index_ = 1;
insertion_index = next_insertion_index_++;
if (PaintTimingVisualizer::IsTracingEnabled()) {
lcp_rect_info_ = std::make_unique<LCPRectInfo>(
frame_visual_rect, RoundedIntRect(root_visual_rect));
}
}
ImageRecord() {}
DOMNodeId node_id = kInvalidDOMNodeId;
WeakPersistent<const ImageResourceContent> cached_image;
// Mind that |first_size| has to be assigned before pusing to
// |size_ordered_set_| since it's the sorting key.
uint64_t first_size = 0;
unsigned frame_index = 0;
unsigned insertion_index;
// The time of the first paint after fully loaded. 0 means not painted yet.
base::TimeTicks paint_time = base::TimeTicks();
base::TimeTicks load_time = base::TimeTicks();
bool loaded = false;
// LCP rect information, only populated when tracing is enabled.
std::unique_ptr<LCPRectInfo> lcp_rect_info_;
};
typedef std::pair<const LayoutObject*, const ImageResourceContent*> RecordId;
// |ImageRecordsManager| is the manager of all of the images that Largest Image
// Paint cares about. Note that an image does not necessarily correspond to a
// node; it can also be one of the background images attached to a node.
// |ImageRecordsManager| encapsulates the logic of |ImageRecord| handling,
// providing interface for the external world to handle it in the language of
// Node, LayoutObject, etc.
class CORE_EXPORT ImageRecordsManager {
friend class ImagePaintTimingDetectorTest;
DISALLOW_NEW();
using NodesQueueComparator = bool (*)(const base::WeakPtr<ImageRecord>&,
const base::WeakPtr<ImageRecord>&);
using ImageRecordSet =
std::set<base::WeakPtr<ImageRecord>, NodesQueueComparator>;
public:
explicit ImageRecordsManager(LocalFrameView*);
ImageRecordsManager(const ImageRecordsManager&) = delete;
ImageRecordsManager& operator=(const ImageRecordsManager&) = delete;
ImageRecord* FindLargestPaintCandidate() const;
inline void RemoveInvisibleRecordIfNeeded(const RecordId& record_id) {
invisible_images_.erase(record_id);
}
inline void RemoveImageFinishedRecord(const RecordId& record_id) {
image_finished_times_.erase(record_id);
}
inline void RemoveVisibleRecord(const RecordId& record_id) {
base::WeakPtr<ImageRecord> record =
visible_images_.find(record_id)->value->AsWeakPtr();
if (!record->paint_time.is_null()) {
DCHECK_GT(record->first_size, 0u);
if (record->first_size > largest_removed_image_size_) {
largest_removed_image_size_ = record->first_size;
largest_removed_image_paint_time_ = record->paint_time;
} else if (record->first_size == largest_removed_image_size_) {
// Ensure we use the lower timestamp in the case of a tie.
DCHECK(!largest_removed_image_paint_time_.is_null());
largest_removed_image_paint_time_ =
std::min(largest_removed_image_paint_time_, record->paint_time);
}
}
size_ordered_set_.erase(record);
visible_images_.erase(record_id);
// Leave out |images_queued_for_paint_time_| intentionally because the null
// record will be removed in |AssignPaintTimeToRegisteredQueuedRecords|.
}
inline void RecordInvisible(const RecordId& record_id) {
invisible_images_.insert(record_id);
}
void RecordVisible(const RecordId& record_id,
const uint64_t& visual_size,
const IntRect& frame_visual_rect,
const FloatRect& root_visual_rect);
bool IsRecordedVisibleImage(const RecordId& record_id) const {
return visible_images_.Contains(record_id);
}
bool IsRecordedInvisibleImage(const RecordId& record_id) const {
return invisible_images_.Contains(record_id);
}
void NotifyImageFinished(const RecordId& record_id) {
// TODO(npm): Ideally NotifyImageFinished() would only be called when the
// record has not yet been inserted in |image_finished_times_| but that's
// not currently the case. If we plumb some information from
// ImageResourceContent we may be able to ensure that this call does not
// require the Contains() check, which would save time.
if (!image_finished_times_.Contains(record_id))
image_finished_times_.insert(record_id, base::TimeTicks::Now());
}
inline bool IsVisibleImageLoaded(const RecordId& record_id) const {
DCHECK(visible_images_.Contains(record_id));
return visible_images_.at(record_id)->loaded;
}
void OnImageLoaded(const RecordId&,
unsigned current_frame_index,
const StyleFetchedImage*);
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,
const IntRect& frame_visual_rect,
const FloatRect& root_visual_rect);
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(
unsigned last_registered_frame_index) {
while (!images_queued_for_paint_time_.IsEmpty() &&
!images_queued_for_paint_time_.back()) {
images_queued_for_paint_time_.pop_back();
}
if (images_queued_for_paint_time_.IsEmpty())
return false;
return last_registered_frame_index < LastQueuedFrameIndex();
}
void AssignPaintTimeToRegisteredQueuedRecords(
const base::TimeTicks&,
unsigned last_queued_frame_index);
inline unsigned LastQueuedFrameIndex() const {
DCHECK(images_queued_for_paint_time_.back());
return images_queued_for_paint_time_.back()->frame_index;
}
uint64_t LargestRemovedImageSize() const {
return largest_removed_image_size_;
}
base::TimeTicks LargestRemovedImagePaintTime() const {
return largest_removed_image_paint_time_;
}
void Trace(Visitor* visitor) const;
private:
// Find the image record of an visible image.
inline base::WeakPtr<ImageRecord> FindVisibleRecord(
const RecordId& record_id) const {
DCHECK(visible_images_.Contains(record_id));
return visible_images_.find(record_id)->value->AsWeakPtr();
}
std::unique_ptr<ImageRecord> CreateImageRecord(
const LayoutObject& object,
const ImageResourceContent* cached_image,
const uint64_t& visual_size,
const IntRect& frame_visual_rect,
const FloatRect& root_visual_rect);
inline void QueueToMeasurePaintTime(base::WeakPtr<ImageRecord>& record,
unsigned current_frame_index) {
images_queued_for_paint_time_.push_back(record);
record->frame_index = current_frame_index;
}
inline void SetLoaded(base::WeakPtr<ImageRecord>& record) {
record->loaded = true;
}
HashMap<RecordId, std::unique_ptr<ImageRecord>> visible_images_;
HashSet<RecordId> invisible_images_;
// This stores the image records, which are ordered by size.
ImageRecordSet size_ordered_set_;
// |ImageRecord|s waiting for paint time are stored in this queue
// until they get a presentation time.
Deque<base::WeakPtr<ImageRecord>> images_queued_for_paint_time_;
// Map containing timestamps of when LayoutObject::ImageNotifyFinished is
// first called.
HashMap<RecordId, base::TimeTicks> image_finished_times_;
Member<LocalFrameView> frame_view_;
// We store the size and paint time of the largest removed image in order to
// compute experimental LCP correctly.
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_;
};
// ImagePaintTimingDetector contains Largest Image Paint.
//
// Largest Image Paint timing measures when the largest image element within
// viewport finishes painting. Specifically, it:
// 1. Tracks all images' first invalidation, recording their visual size, if
// this image is within viewport.
// 2. When an image finishes loading, record its paint time.
// 3. At the end of each frame, if new images are added and loaded, the
// algorithm will start an analysis.
//
// In the analysis:
// 3.1 Largest Image Paint finds the largest image by the first visual size. If
// it has finished loading, reports a candidate result as its first paint time
// since loaded.
//
// For all these candidate results, Telemetry picks the lastly reported
// Largest Image Paint candidate as its final result.
//
// See also:
// https://docs.google.com/document/d/1DRVd4a2VU8-yyWftgOparZF-sf16daf0vfbsHuz2rws/edit#heading=h.1k2rnrs6mdmt
class CORE_EXPORT ImagePaintTimingDetector final
: public GarbageCollected<ImagePaintTimingDetector> {
friend class ImagePaintTimingDetectorTest;
public:
ImagePaintTimingDetector(LocalFrameView*, PaintTimingCallbackManager*);
// Record an image paint. This method covers both img and background image. In
// the case of a normal img, the last parameter will be nullptr. This
// parameter is needed only for the purposes of plumbing the correct loadTime
// value to the ImageRecord.
void RecordImage(const LayoutObject&,
const IntSize& intrinsic_size,
const ImageResourceContent&,
const PropertyTreeStateOrAlias& current_paint_properties,
const StyleFetchedImage*,
const IntRect& image_border);
void NotifyImageFinished(const LayoutObject&, const ImageResourceContent*);
void OnPaintFinished();
void NotifyImageRemoved(const LayoutObject&, const ImageResourceContent*);
// After the method being called, the detector stops to record new entries and
// node removal. But it still observe the loading status. In other words, if
// an image is recorded before stopping recording, and finish loading after
// stopping recording, the detector can still observe the loading being
// finished.
void StopRecordEntries();
inline bool IsRecording() const { return is_recording_; }
inline bool FinishedReportingImages() const {
return !is_recording_ && num_pending_presentation_callbacks_ == 0;
}
void ResetCallbackManager(PaintTimingCallbackManager* manager) {
callback_manager_ = manager;
}
void ReportPresentationTime(unsigned last_queued_frame_index,
base::TimeTicks);
// 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:
friend class LargestContentfulPaintCalculatorTest;
void PopulateTraceValue(TracedValue&, const ImageRecord& first_image_paint);
void RegisterNotifyPresentationTime();
void ReportCandidateToTrace(ImageRecord&);
void ReportNoCandidateToTrace();
// Computes the size of an image for the purpose of LargestContentfulPaint,
// downsizing the size of images with low intrinsic size. Images that occupy
// the full viewport are special-cased and this method returns 0 for them so
// that they are not considered valid candidates.
uint64_t ComputeImageRectSize(const IntRect& image_border,
const FloatRect& mapped_visual_rect,
const IntSize&,
const PropertyTreeStateOrAlias&,
const LayoutObject&,
const ImageResourceContent&);
// Used to find the last candidate.
unsigned count_candidates_ = 0;
// Used to decide which frame a record belongs to, monotonically increasing.
unsigned frame_index_ = 1;
unsigned last_registered_frame_index_ = 0;
// Used to control if we record new image entries and image removal, but has
// no effect on recording the loading status.
bool is_recording_ = true;
// Used to determine how many presentation callbacks are pending. In
// combination with |is_recording|, helps determine whether this detector can
// be destroyed.
int num_pending_presentation_callbacks_ = 0;
// This need to be set whenever changes that can affect the output of
// |FindLargestPaintCandidate| occur during the paint tree walk.
bool need_update_timing_at_frame_end_ = false;
bool contains_full_viewport_image_ = false;
// We cache the viewport size computation to avoid performing it on every
// image. This value is reset when paint is finished and is computed if unset
// when needed. 0 means that the size has not been computed.
base::Optional<uint64_t> viewport_size_;
ImageRecordsManager records_manager_;
Member<LocalFrameView> frame_view_;
Member<PaintTimingCallbackManager> callback_manager_;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_IMAGE_PAINT_TIMING_DETECTOR_H_