blob: eb6a61f486ab7d3fb15d95ba36be3663e83823b8 [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/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/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"
namespace blink {
// Calculate metrics candidate every 1 second after the first text pre-paint.
static constexpr TimeDelta kTimerDelay = TimeDelta::FromSeconds(1);
constexpr size_t kTextNodeNumberLimit = 5000;
static bool LargeTextOnTop(const std::unique_ptr<TextRecord>& a,
const std::unique_ptr<TextRecord>& b) {
return a->first_size < b->first_size;
}
static bool LateTextOnTop(const std::unique_ptr<TextRecord>& a,
const std::unique_ptr<TextRecord>& b) {
return a->first_paint_time < b->first_paint_time;
}
TextPaintTimingDetector::TextPaintTimingDetector(LocalFrameView* frame_view)
: largest_text_heap_(&LargeTextOnTop),
latest_text_heap_(&LateTextOnTop),
timer_(frame_view->GetFrame().GetTaskRunner(TaskType::kInternalDefault),
this,
&TextPaintTimingDetector::TimerFired),
frame_view_(frame_view) {}
void TextPaintTimingDetector::PopulateTraceValue(
TracedValue& value,
const TextRecord& first_text_paint,
unsigned candidate_index) const {
value.SetInteger("DOMNodeId", static_cast<int>(first_text_paint.node_id));
#ifndef NDEBUG
value.SetString("text", first_text_paint.text);
#endif
value.SetInteger("size", static_cast<int>(first_text_paint.first_size));
value.SetInteger("candidateIndex", candidate_index);
value.SetString("frame",
IdentifiersFactory::FrameId(&frame_view_->GetFrame()));
}
void TextPaintTimingDetector::OnLargestTextDetected(
const TextRecord& largest_text_record) {
largest_text_paint_ = largest_text_record.first_paint_time;
largest_text_paint_size_ = largest_text_record.first_size;
std::unique_ptr<TracedValue> value = TracedValue::Create();
PopulateTraceValue(*value, largest_text_record,
largest_text_candidate_index_max_++);
TRACE_EVENT_INSTANT_WITH_TIMESTAMP1(
"loading", "LargestTextPaint::Candidate", TRACE_EVENT_SCOPE_THREAD,
largest_text_paint_, "data", std::move(value));
}
void TextPaintTimingDetector::OnLastTextDetected(
const TextRecord& last_text_record) {
last_text_paint_ = last_text_record.first_paint_time;
last_text_paint_size_ = last_text_record.first_size;
std::unique_ptr<TracedValue> value = TracedValue::Create();
PopulateTraceValue(*value, last_text_record,
last_text_candidate_index_max_++);
TRACE_EVENT_INSTANT_WITH_TIMESTAMP1(
"loading", "LastTextPaint::Candidate", TRACE_EVENT_SCOPE_THREAD,
last_text_paint_, "data", std::move(value));
}
void TextPaintTimingDetector::TimerFired(TimerBase* time) {
// Wrap Analyze method in TimerFired so that we can drop |time| for Analyze
// in testing.
Analyze();
}
void TextPaintTimingDetector::Analyze() {
TextRecord* largest_text_first_paint = FindLargestPaintCandidate();
bool new_candidate_detected = false;
DCHECK(!largest_text_first_paint ||
!largest_text_first_paint->first_paint_time.is_null());
if (largest_text_first_paint &&
largest_text_first_paint->first_paint_time != largest_text_paint_) {
OnLargestTextDetected(*largest_text_first_paint);
new_candidate_detected = true;
}
TextRecord* last_text_first_paint = FindLastPaintCandidate();
DCHECK(!last_text_first_paint ||
!last_text_first_paint->first_paint_time.is_null());
if (last_text_first_paint &&
last_text_first_paint->first_paint_time != last_text_paint_) {
OnLastTextDetected(*last_text_first_paint);
new_candidate_detected = true;
}
if (new_candidate_detected) {
frame_view_->GetPaintTimingDetector().DidChangePerformanceTiming();
}
}
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 (!is_recording_)
return;
for (TextRecord& record : texts_to_record_swap_time_) {
if (record.node_id == node_id)
record.node_id = kInvalidDOMNodeId;
}
if (recorded_text_node_ids_.find(node_id) == recorded_text_node_ids_.end())
return;
// We assume that removed nodes' id would not be recycled, and it's expensive
// to remove records from largest_text_heap_ and latest_text_heap_, so we
// intentionally keep the records of removed nodes in largest_text_heap_ and
// latest_text_heap_.
recorded_text_node_ids_.erase(node_id);
if (recorded_text_node_ids_.size() == 0) {
const bool largest_text_paint_invalidated =
largest_text_paint_ != base::TimeTicks();
const bool last_text_paint_invalidated =
last_text_paint_ != base::TimeTicks();
if (largest_text_paint_invalidated)
largest_text_paint_ = base::TimeTicks();
if (last_text_paint_invalidated)
last_text_paint_ = base::TimeTicks();
if (largest_text_paint_invalidated || last_text_paint_invalidated) {
frame_view_->GetPaintTimingDetector().DidChangePerformanceTiming();
}
}
}
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_) {
if (record.node_id == kInvalidDOMNodeId)
continue;
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 PropertyTreeState& current_paint_chunk_properties) {
if (!is_recording_)
return;
Node* node = object.GetNode();
if (!node)
return;
DOMNodeId node_id = DOMNodeIds::IdForNode(node);
// This metric defines the size of a text by its first size. So it
// early-returns if the text has been recorded.
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.
uint64_t rect_size = 0;
// Compared to object.FirstFragment().VisualRect(), this will include other
// fragments of the object.
LayoutRect visual_rect = object.FragmentsVisualRectBoundingBox();
if (!visual_rect.IsEmpty()) {
rect_size = frame_view_->GetPaintTimingDetector().CalculateVisualSize(
visual_rect, current_paint_chunk_properties);
}
// When rect_size == 0, it either means the text size is 0 or the text is out
// of viewport. In either case, we don't record their time for efficiency.
if (rect_size == 0) {
size_zero_node_ids_.insert(node_id);
} else {
// Non-trivial text is found.
TextRecord record;
record.node_id = node_id;
record.first_size = rect_size;
#ifndef NDEBUG
record.text = ToLayoutText(&object)->GetText();
#endif
texts_to_record_swap_time_.push_back(record);
}
if (recorded_text_node_ids_.size() + size_zero_node_ids_.size() +
texts_to_record_swap_time_.size() >=
kTextNodeNumberLimit) {
TRACE_EVENT_INSTANT2("loading", "TextPaintTimingDetector::OverNodeLimit",
TRACE_EVENT_SCOPE_THREAD, "recorded_node_count",
recorded_text_node_ids_.size(), "size_zero_node_count",
size_zero_node_ids_.size());
StopRecordEntries();
}
}
void TextPaintTimingDetector::StopRecordEntries() {
timer_.Stop();
is_recording_ = false;
}
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