| // 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/text_paint_timing_detector.h" |
| |
| #include <memory> |
| |
| #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_file_upload_control.h" |
| #include "third_party/blink/renderer/core/layout/layout_text.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/paint/largest_contentful_paint_calculator.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/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" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| constexpr size_t kTextNodeNumberLimit = 5000; |
| |
| bool LargeTextFirst(const base::WeakPtr<TextRecord>& a, |
| const base::WeakPtr<TextRecord>& 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 nodes with the same |first_size| wouldn't |
| // be merged in the set. |
| return a->node_id > b->node_id; |
| } |
| |
| } // namespace |
| |
| TextPaintTimingDetector::TextPaintTimingDetector( |
| LocalFrameView* frame_view, |
| PaintTimingDetector* paint_timing_detector) |
| : records_manager_(frame_view, paint_timing_detector), |
| frame_view_(frame_view) {} |
| |
| void LargestTextPaintManager::PopulateTraceValue( |
| TracedValue& value, |
| const TextRecord& first_text_paint) { |
| // TODO(crbug.com/976893): Remove DOMNodeId. |
| value.SetInteger("DOMNodeId", static_cast<int>(first_text_paint.node_id)); |
| value.SetInteger("size", static_cast<int>(first_text_paint.first_size)); |
| value.SetInteger("candidateIndex", ++count_candidates_); |
| value.SetBoolean("isMainFrame", frame_view_->GetFrame().IsMainFrame()); |
| value.SetBoolean("isOOPIF", |
| !frame_view_->GetFrame().LocalFrameRoot().IsMainFrame()); |
| } |
| |
| void LargestTextPaintManager::ReportCandidateToTrace( |
| const TextRecord& largest_text_record) { |
| if (!PaintTimingDetector::IsTracing()) |
| return; |
| auto value = std::make_unique<TracedValue>(); |
| PopulateTraceValue(*value, largest_text_record); |
| TRACE_EVENT_MARK_WITH_TIMESTAMP2("loading", "LargestTextPaint::Candidate", |
| largest_text_record.paint_time, "data", |
| std::move(value), "frame", |
| ToTraceValue(&frame_view_->GetFrame())); |
| } |
| |
| void LargestTextPaintManager::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", "LargestTextPaint::NoCandidate", "data", |
| std::move(value), "frame", |
| ToTraceValue(&frame_view_->GetFrame())); |
| } |
| |
| void LargestTextPaintManager::UpdateCandidate() { |
| base::WeakPtr<TextRecord> largest_text_record = FindLargestPaintCandidate(); |
| const base::TimeTicks time = |
| largest_text_record ? largest_text_record->paint_time : base::TimeTicks(); |
| const uint64_t size = |
| largest_text_record ? largest_text_record->first_size : 0; |
| DCHECK(paint_timing_detector_); |
| bool changed = |
| paint_timing_detector_->NotifyIfChangedLargestTextPaint(time, size); |
| if (!changed) |
| return; |
| |
| if (!time.is_null()) { |
| if (auto* lcp_calculator = |
| paint_timing_detector_->GetLargestContentfulPaintCalculator()) |
| lcp_calculator->OnLargestTextUpdated(largest_text_record); |
| ReportCandidateToTrace(*largest_text_record); |
| } else { |
| if (auto* lcp_calculator = |
| paint_timing_detector_->GetLargestContentfulPaintCalculator()) |
| lcp_calculator->OnLargestTextUpdated(nullptr); |
| ReportNoCandidateToTrace(); |
| } |
| } |
| |
| void TextPaintTimingDetector::OnPaintFinished() { |
| if (need_update_timing_at_frame_end_) { |
| need_update_timing_at_frame_end_ = false; |
| if (records_manager_.GetLargestTextPaintManager()) |
| records_manager_.GetLargestTextPaintManager()->UpdateCandidate(); |
| } |
| if (records_manager_.NeedMeausuringPaintTime()) { |
| if (!awaiting_swap_promise_) { |
| // |WrapCrossThreadWeakPersistent| guarantees that when |this| is killed, |
| // the callback function will not be invoked. |
| RegisterNotifySwapTime( |
| CrossThreadBindOnce(&TextPaintTimingDetector::ReportSwapTime, |
| WrapCrossThreadWeakPersistent(this))); |
| } |
| } |
| } |
| |
| void TextPaintTimingDetector::LayoutObjectWillBeDestroyed( |
| const LayoutObject& object) { |
| if (!is_recording_) |
| return; |
| if (records_manager_.IsKnownVisible(object)) { |
| records_manager_.RemoveVisibleRecord(object); |
| need_update_timing_at_frame_end_ = true; |
| } else if (records_manager_.IsKnownInvisible(object)) { |
| records_manager_.RemoveInvisibleRecord(object); |
| need_update_timing_at_frame_end_ = true; |
| } |
| } |
| |
| void TextPaintTimingDetector::RegisterNotifySwapTime( |
| ReportTimeCallback callback) { |
| // 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; |
| frame.GetPage()->GetChromeClient().NotifySwapTime(frame, std::move(callback)); |
| awaiting_swap_promise_ = true; |
| } |
| |
| void TextPaintTimingDetector::ReportSwapTime(WebWidgetClient::SwapResult result, |
| base::TimeTicks timestamp) { |
| if (!is_recording_) |
| return; |
| if (!records_manager_.HasTextElementTiming()) { |
| Document* document = frame_view_->GetFrame().GetDocument(); |
| if (document && RuntimeEnabledFeatures::ElementTimingEnabled(document)) { |
| LocalDOMWindow* window = document->domWindow(); |
| if (window) { |
| records_manager_.SetTextElementTiming( |
| &TextElementTiming::From(*window)); |
| } |
| } |
| } |
| records_manager_.AssignPaintTimeToQueuedNodes(timestamp); |
| if (records_manager_.GetLargestTextPaintManager()) |
| records_manager_.GetLargestTextPaintManager()->UpdateCandidate(); |
| awaiting_swap_promise_ = false; |
| } |
| |
| bool TextPaintTimingDetector::ShouldWalkObject( |
| const LayoutBoxModelObject& object) const { |
| if (!is_recording_) |
| return false; |
| // TODO(crbug.com/933479): Use LayoutObject::GeneratingNode() to include |
| // anonymous objects' rect. |
| Node* node = object.GetNode(); |
| if (!node) |
| return false; |
| // If we have finished recording Largest Text Paint and the element is a |
| // shadow element or has no elementtiming attribute, then we should not record |
| // its text. |
| if (!records_manager_.IsRecordingLargestTextPaint() && |
| !TextElementTiming::NeededForElementTiming(node)) |
| return false; |
| |
| DOMNodeId node_id = DOMNodeIds::ExistingIdForNode(node); |
| if (node_id == kInvalidDOMNodeId) |
| return true; |
| // This metric defines the size of a text block by its first size, so we |
| // should not walk the object if it has been recorded. |
| return !records_manager_.HasRecorded(object); |
| } |
| |
| void TextPaintTimingDetector::RecordAggregatedText( |
| const LayoutBoxModelObject& aggregator, |
| const IntRect& aggregated_visual_rect, |
| const PropertyTreeState& property_tree_state) { |
| DCHECK(ShouldWalkObject(aggregator)); |
| DCHECK(!records_manager_.HasTooManyNodes()); |
| |
| Node* node = aggregator.GetNode(); |
| DCHECK(node); |
| DOMNodeId node_id = DOMNodeIds::IdForNode(node); |
| DCHECK_NE(node_id, kInvalidDOMNodeId); |
| |
| // The caller should check this. |
| DCHECK(!aggregated_visual_rect.IsEmpty()); |
| |
| FloatRect mapped_visual_rect = |
| frame_view_->GetPaintTimingDetector().CalculateVisualRect( |
| aggregated_visual_rect, property_tree_state); |
| uint64_t aggregated_size = mapped_visual_rect.Size().Area(); |
| |
| if (aggregated_size == 0) { |
| records_manager_.RecordInvisibleObject(aggregator); |
| } else { |
| records_manager_.RecordVisibleObject( |
| aggregator, aggregated_size, |
| TextElementTiming::ComputeIntersectionRect( |
| node, aggregated_visual_rect, property_tree_state, frame_view_), |
| node_id); |
| } |
| |
| if (records_manager_.HasTooManyNodes()) { |
| TRACE_EVENT_INSTANT2("loading", "TextPaintTimingDetector::OverNodeLimit", |
| TRACE_EVENT_SCOPE_THREAD, "count_size_non_zero_nodes", |
| records_manager_.CountVisibleObjects(), |
| "count_size_zero_nodes", |
| records_manager_.CountInvisibleObjects()); |
| StopRecordEntries(); |
| } |
| } |
| |
| void TextPaintTimingDetector::StopRecordEntries() { |
| is_recording_ = false; |
| records_manager_.CleanUp(); |
| } |
| |
| void TextPaintTimingDetector::StopRecordingLargestTextPaint() { |
| records_manager_.CleanUpLargestTextPaint(); |
| } |
| |
| void TextPaintTimingDetector::Trace(blink::Visitor* visitor) { |
| visitor->Trace(records_manager_); |
| visitor->Trace(frame_view_); |
| } |
| |
| LargestTextPaintManager::LargestTextPaintManager( |
| LocalFrameView* frame_view, |
| PaintTimingDetector* paint_timing_detector) |
| : size_ordered_set_(&LargeTextFirst), |
| frame_view_(frame_view), |
| paint_timing_detector_(paint_timing_detector) {} |
| |
| void LargestTextPaintManager::Trace(blink::Visitor* visitor) { |
| visitor->Trace(frame_view_); |
| visitor->Trace(paint_timing_detector_); |
| } |
| |
| void TextRecordsManager::RemoveVisibleRecord(const LayoutObject& object) { |
| DCHECK(visible_node_map_.Contains(&object)); |
| if (ltp_manager_) { |
| ltp_manager_->RemoveVisibleRecord( |
| visible_node_map_.at(&object)->AsWeakPtr()); |
| } |
| visible_node_map_.erase(&object); |
| // We don't need to remove elements in |texts_queued_for_paint_time_| and |
| // |cached_largest_paint_candidate_| as they are weak ptr. |
| } |
| |
| void TextRecordsManager::CleanUpLargestTextPaint() { |
| ltp_manager_.reset(); |
| } |
| |
| void TextRecordsManager::RemoveInvisibleRecord(const LayoutObject& object) { |
| DCHECK(invisible_node_ids_.Contains(&object)); |
| invisible_node_ids_.erase(&object); |
| } |
| |
| void TextRecordsManager::AssignPaintTimeToQueuedNodes( |
| const base::TimeTicks& timestamp) { |
| // If texts_queued_for_paint_time_.size == 0, it means the array has been |
| // consumed in a callback earlier than this one. That violates the assumption |
| // that only one or zero callback will be called after one OnPaintFinished. |
| DCHECK_GT(texts_queued_for_paint_time_.size(), 0UL); |
| for (auto iterator = texts_queued_for_paint_time_.begin(); |
| iterator != texts_queued_for_paint_time_.end(); ++iterator) { |
| // The record may have been removed between the callback registration and |
| // invoking. |
| base::WeakPtr<TextRecord>& record = *iterator; |
| if (!record) { |
| texts_queued_for_paint_time_.erase(iterator); |
| continue; |
| } |
| DCHECK_EQ(record->paint_time, base::TimeTicks()); |
| record->paint_time = timestamp; |
| } |
| if (text_element_timing_) |
| text_element_timing_->OnTextNodesPainted(texts_queued_for_paint_time_); |
| texts_queued_for_paint_time_.clear(); |
| if (ltp_manager_) |
| ltp_manager_->SetCachedResultInvalidated(true); |
| } |
| |
| void TextRecordsManager::RecordVisibleObject( |
| const LayoutObject& object, |
| const uint64_t& visual_size, |
| const FloatRect& element_timing_rect, |
| DOMNodeId node_id) { |
| DCHECK(!HasTooManyNodes()); |
| DCHECK_GT(visual_size, 0u); |
| |
| std::unique_ptr<TextRecord> record = |
| std::make_unique<TextRecord>(node_id, visual_size, element_timing_rect); |
| base::WeakPtr<TextRecord> record_weak_ptr = record->AsWeakPtr(); |
| if (ltp_manager_) |
| ltp_manager_->InsertRecord(record_weak_ptr); |
| |
| QueueToMeasurePaintTime(record_weak_ptr); |
| visible_node_map_.insert(&object, std::move(record)); |
| } |
| |
| bool TextRecordsManager::HasTooManyNodes() const { |
| return visible_node_map_.size() + invisible_node_ids_.size() >= |
| kTextNodeNumberLimit; |
| } |
| |
| base::WeakPtr<TextRecord> LargestTextPaintManager::FindLargestPaintCandidate() { |
| if (!is_result_invalidated_ && cached_largest_paint_candidate_) |
| return cached_largest_paint_candidate_; |
| base::WeakPtr<TextRecord> new_largest_paint_candidate = nullptr; |
| for (const auto& text_record : size_ordered_set_) { |
| DCHECK(text_record); |
| if (text_record->paint_time.is_null()) |
| continue; |
| new_largest_paint_candidate = text_record; |
| break; |
| } |
| cached_largest_paint_candidate_ = new_largest_paint_candidate; |
| is_result_invalidated_ = false; |
| return new_largest_paint_candidate; |
| } |
| |
| TextRecordsManager::TextRecordsManager( |
| LocalFrameView* frame_view, |
| PaintTimingDetector* paint_timing_detector) { |
| if (RuntimeEnabledFeatures::FirstContentfulPaintPlusPlusEnabled()) |
| ltp_manager_.emplace(frame_view, paint_timing_detector); |
| } |
| |
| void TextRecordsManager::CleanUp() { |
| visible_node_map_.clear(); |
| invisible_node_ids_.clear(); |
| texts_queued_for_paint_time_.clear(); |
| CleanUpLargestTextPaint(); |
| } |
| |
| void TextRecordsManager::Trace(blink::Visitor* visitor) { |
| visitor->Trace(text_element_timing_); |
| visitor->Trace(ltp_manager_); |
| } |
| |
| } // namespace blink |