| // 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 "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_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/paint_layer.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 { |
| |
| // Calculate metrics candidate every 1 second after the first text pre-paint. |
| static constexpr TimeDelta kTimerDelay = TimeDelta::FromSeconds(1); |
| |
| static bool LargeOnTop(std::unique_ptr<TextRecord>& a, |
| std::unique_ptr<TextRecord>& b) { |
| return a->first_size < b->first_size; |
| } |
| |
| static bool LateOnTop(std::unique_ptr<TextRecord>& a, |
| std::unique_ptr<TextRecord>& b) { |
| return a->first_paint_time < b->first_paint_time; |
| } |
| |
| TextPaintTimingDetector::TextPaintTimingDetector(LocalFrameView* frame_view) |
| : largest_text_heap_(LargeOnTop), |
| latest_text_heap_(LateOnTop), |
| timer_(frame_view->GetFrame().GetTaskRunner(TaskType::kInternalDefault), |
| this, |
| &TextPaintTimingDetector::TimerFired), |
| frame_view_(frame_view) {} |
| |
| void TextPaintTimingDetector::PopulateTraceValue( |
| std::unique_ptr<TracedValue>& value, |
| TextRecord* first_text_paint, |
| int report_index) { |
| value->SetInteger("DOMNodeId", first_text_paint->node_id); |
| value->SetString("text", first_text_paint->text); |
| value->SetInteger("size", first_text_paint->first_size); |
| value->SetInteger("reportIndex", report_index); |
| value->SetString("frame", |
| IdentifiersFactory::FrameId(&frame_view_->GetFrame())); |
| } |
| |
| IntRect TextPaintTimingDetector::CalculateTransformedRect( |
| LayoutRect& invalidated_rect, |
| const PaintLayer& painting_layer) { |
| 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(); |
| GeometryMapper::SourceToDestinationRect(local_transform, ancestor_transform, |
| invalidated_rect_abs); |
| IntRect invalidated_rect_in_viewport = RoundedIntRect(invalidated_rect_abs); |
| invalidated_rect_in_viewport.Intersect( |
| frame_view_->GetScrollableArea()->VisibleContentRect()); |
| return invalidated_rect_in_viewport; |
| } |
| |
| void TextPaintTimingDetector::TimerFired(TimerBase* timer) { |
| if (TextRecord* largest_text_first_paint = FindLargestPaintCandidate()) { |
| std::unique_ptr<TracedValue> value = TracedValue::Create(); |
| PopulateTraceValue(value, largest_text_first_paint, |
| largest_text_report_count_++); |
| TRACE_EVENT_INSTANT_WITH_TIMESTAMP1( |
| "loading", "LargestTextPaint::Candidate", TRACE_EVENT_SCOPE_THREAD, |
| largest_text_first_paint->first_paint_time, "data", std::move(value)); |
| } |
| if (TextRecord* last_text_first_paint = FindLastPaintCandidate()) { |
| std::unique_ptr<TracedValue> value = TracedValue::Create(); |
| PopulateTraceValue(value, last_text_first_paint, last_text_report_count_++); |
| TRACE_EVENT_INSTANT_WITH_TIMESTAMP1( |
| "loading", "LastTextPaint::Candidate", TRACE_EVENT_SCOPE_THREAD, |
| last_text_first_paint->first_paint_time, "data", std::move(value)); |
| } |
| } |
| |
| void TextPaintTimingDetector::OnPrePaintFinished() { |
| if (texts_to_record_swap_time_.size() > 0) { |
| // Start repeating timer only once after the first text prepaint. |
| if (!timer_.IsActive()) { |
| timer_.StartRepeating(kTimerDelay, FROM_HERE); |
| } |
| if (!awaiting_swap_promise_) { |
| RegisterNotifySwapTime( |
| CrossThreadBind(&TextPaintTimingDetector::ReportSwapTime, |
| WrapCrossThreadWeakPersistent(this))); |
| } |
| } |
| } |
| |
| void TextPaintTimingDetector::NotifyNodeRemoved(DOMNodeId node_id) { |
| if (recorded_text_node_ids_.find(node_id) != recorded_text_node_ids_.end()) { |
| // We assume that the removed node's id wouldn't be recycled, so we don't |
| // bother to remove these records from largest_text_heap_ or |
| // latest_text_heap_, to reduce computation. |
| recorded_text_node_ids_.erase(node_id); |
| } |
| } |
| |
| 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; |
| if (WebLayerTreeView* layerTreeView = |
| frame.GetPage()->GetChromeClient().GetWebLayerTreeView(&frame)) { |
| layerTreeView->NotifySwapTime(ConvertToBaseCallback(std::move(callback))); |
| awaiting_swap_promise_ = true; |
| } |
| } |
| |
| void TextPaintTimingDetector::ReportSwapTime( |
| WebLayerTreeView::SwapResult result, |
| base::TimeTicks timestamp) { |
| // If texts_to_record_swap_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 OnPrePaintFinished. |
| DCHECK_GT(texts_to_record_swap_time_.size(), 0UL); |
| for (TextRecord& record : texts_to_record_swap_time_) { |
| record.first_paint_time = timestamp; |
| recorded_text_node_ids_.insert(record.node_id); |
| largest_text_heap_.push(std::make_unique<TextRecord>(record)); |
| latest_text_heap_.push(std::make_unique<TextRecord>(record)); |
| } |
| texts_to_record_swap_time_.clear(); |
| awaiting_swap_promise_ = false; |
| } |
| |
| void TextPaintTimingDetector::RecordText(const LayoutObject& object, |
| const PaintLayer& painting_layer) { |
| Node* node = object.GetNode(); |
| if (!node) |
| return; |
| DOMNodeId node_id = DOMNodeIds::IdForNode(node); |
| |
| if (size_zero_node_ids_.find(node_id) != size_zero_node_ids_.end()) |
| return; |
| if (recorded_text_node_ids_.find(node_id) != recorded_text_node_ids_.end()) |
| return; |
| |
| // When node_id is not found in recorded_text_node_ids_, this invalidation is |
| // the text's first invalidation. |
| LayoutRect invalidated_rect = object.FirstFragment().VisualRect(); |
| int rect_size = 0; |
| if (!invalidated_rect.IsEmpty()) { |
| IntRect invalidated_rect_in_viewport = |
| CalculateTransformedRect(invalidated_rect, painting_layer); |
| rect_size = invalidated_rect_in_viewport.Height() * |
| invalidated_rect_in_viewport.Width(); |
| } |
| // When rect_size == 0, it either means invalidated_rect.IsEmpty() or |
| // the text is size 0 or the text is out of viewport. Either way, we don't |
| // record their time, to reduce computation. |
| if (rect_size == 0) { |
| size_zero_node_ids_.insert(node_id); |
| } else { |
| TextRecord record = {node_id, rect_size, base::TimeTicks(), |
| ToLayoutText(&object)->GetText()}; |
| texts_to_record_swap_time_.push_back(record); |
| } |
| } |
| |
| TextRecord* TextPaintTimingDetector::FindLargestPaintCandidate() { |
| while (!largest_text_heap_.empty() && |
| !recorded_text_node_ids_.Contains(largest_text_heap_.top()->node_id)) { |
| // If recorded_text_node_ids_ doesn't have record.node_id, the node has |
| // been deleted. We discard the records of deleted node. |
| largest_text_heap_.pop(); |
| } |
| if (!largest_text_heap_.empty()) |
| return largest_text_heap_.top().get(); |
| return nullptr; |
| } |
| |
| TextRecord* TextPaintTimingDetector::FindLastPaintCandidate() { |
| while (!latest_text_heap_.empty() && |
| !recorded_text_node_ids_.Contains(latest_text_heap_.top()->node_id)) { |
| // If recorded_text_node_ids_ doesn't have record.node_id, the node has |
| // been deleted. We discard the records of deleted node. |
| latest_text_heap_.pop(); |
| } |
| if (!latest_text_heap_.empty()) |
| return latest_text_heap_.top().get(); |
| return nullptr; |
| } |
| |
| void TextPaintTimingDetector::Trace(blink::Visitor* visitor) { |
| visitor->Trace(frame_view_); |
| } |
| } // namespace blink |