blob: f035b42e58a73b1ab1b2f13cd36b0dc6456f8763 [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_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/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 LargeTextFirst(const base::WeakPtr<TextRecord>& a,
const base::WeakPtr<TextRecord>& b) {
return a->first_size > b->first_size;
}
TextPaintTimingDetector::TextPaintTimingDetector(LocalFrameView* frame_view)
: size_ordered_set_(&LargeTextFirst),
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()));
value.SetBoolean("isMainFrame", frame_view_->GetFrame().IsMainFrame());
value.SetBoolean("isOOPIF",
!frame_view_->GetFrame().LocalFrameRoot().IsMainFrame());
}
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::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();
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);
frame_view_->GetPaintTimingDetector().DidChangePerformanceTiming();
}
}
void TextPaintTimingDetector::OnPaintFinished() {
if (!texts_to_record_swap_time_.empty()) {
// Start repeating timer only once after the first text paint.
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;
if (id_record_map_.find(node_id) == id_record_map_.end())
return;
detached_ids_.insert(node_id);
if (id_record_map_.size() == detached_ids_.size() &&
largest_text_paint_ != base::TimeTicks()) {
largest_text_paint_ = base::TimeTicks();
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 OnPaintFinished.
DCHECK_GT(texts_to_record_swap_time_.size(), 0UL);
while (!texts_to_record_swap_time_.empty()) {
DOMNodeId node_id = texts_to_record_swap_time_.front();
DCHECK(id_record_map_.Contains(node_id));
TextRecord* record = id_record_map_.at(node_id);
record->first_paint_time = timestamp;
size_ordered_set_.insert(record->AsWeakPtr());
texts_to_record_swap_time_.pop();
}
awaiting_swap_promise_ = false;
}
void TextPaintTimingDetector::RecordText(
const LayoutObject& object,
const PropertyTreeState& current_paint_chunk_properties) {
if (!is_recording_)
return;
// TODO(crbug.com/933479): Use LayoutObject::GeneratingNode() to include
// anonymous objects' rect.
Node* node = object.GetNode();
if (!node)
return;
DOMNodeId node_id = DOMNodeIds::IdForNode(node);
DCHECK_NE(node_id, kInvalidDOMNodeId);
// 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_.Contains(node_id))
return;
// The node is reattached.
if (id_record_map_.Contains(node_id) && detached_ids_.Contains(node_id))
detached_ids_.erase(node_id);
if (id_record_map_.Contains(node_id))
return;
// When node_id is not found in id_record_map_, 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);
}
DVLOG(2) << "Node id (" << node_id << "): size=" << rect_size
<< ", type=" << object.DebugName();
// 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.
std::unique_ptr<TextRecord> record = std::make_unique<TextRecord>();
record->node_id = node_id;
record->first_size = rect_size;
#ifndef NDEBUG
if (object.IsText()) {
record->text = ToLayoutText(&object)->GetText();
} else if (object.IsFileUploadControl()) {
record->text = ToLayoutFileUploadControl(&object)->FileTextValue();
} else {
record->text = String("NON-TEXT_OBJECT");
}
#endif
id_record_map_.insert(node_id, std::move(record));
texts_to_record_swap_time_.push(node_id);
}
if (id_record_map_.size() + size_zero_node_ids_.size() >=
kTextNodeNumberLimit) {
TRACE_EVENT_INSTANT2("loading", "TextPaintTimingDetector::OverNodeLimit",
TRACE_EVENT_SCOPE_THREAD, "recorded_node_count",
id_record_map_.size(), "size_zero_node_count",
size_zero_node_ids_.size());
StopRecordEntries();
}
}
void TextPaintTimingDetector::StopRecordEntries() {
timer_.Stop();
is_recording_ = false;
}
TextRecord* TextPaintTimingDetector::FindLargestPaintCandidate() {
return FindCandidate(size_ordered_set_);
}
TextRecord* TextPaintTimingDetector::FindCandidate(
const TextRecordSet& ordered_set) {
for (auto it = ordered_set.begin(); it != ordered_set.end(); ++it) {
if (detached_ids_.Contains((*it)->node_id) ||
(*it)->first_paint_time.is_null())
continue;
DCHECK(id_record_map_.Contains((*it)->node_id));
return (*it).get();
}
return nullptr;
}
void TextPaintTimingDetector::Trace(blink::Visitor* visitor) {
visitor->Trace(frame_view_);
}
} // namespace blink