| // 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/paint_timing_detector.h" |
| |
| #include "third_party/blink/public/platform/web_input_event.h" |
| #include "third_party/blink/renderer/core/dom/document.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/frame/web_frame_widget_base.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/layout/layout_box_model_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/core/loader/resource/image_resource_content.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/image_paint_timing_detector.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/text_paint_timing_detector.h" |
| #include "third_party/blink/renderer/core/style/style_fetched_image.h" |
| #include "third_party/blink/renderer/core/timing/dom_window_performance.h" |
| #include "third_party/blink/renderer/platform/geometry/int_rect.h" |
| #include "third_party/blink/renderer/platform/graphics/image.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/float_clip_rect.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/property_tree_state.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/scoped_paint_chunk_properties.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // In the context of FCP++, we define contentful background image as one that |
| // satisfies all of the following conditions: |
| // * has image reources attached to style of the object, i.e., |
| // { background-image: url('example.gif') } |
| // * not attached to <body> or <html> |
| // This function contains the above heuristics. |
| bool IsBackgroundImageContentful(const LayoutObject& object, |
| const Image& image) { |
| // Background images attached to <body> or <html> are likely for background |
| // purpose, so we rule them out. |
| if (object.IsLayoutView() || object.IsBody() || object.IsDocumentElement()) { |
| return false; |
| } |
| // Generated images are excluded here, as they are likely to serve for |
| // background purpose. |
| if (!image.IsBitmapImage() && !image.IsStaticBitmapImage() && |
| !image.IsSVGImage() && !image.IsPlaceholderImage()) |
| return false; |
| return true; |
| } |
| |
| } // namespace |
| |
| PaintTimingDetector::PaintTimingDetector(LocalFrameView* frame_view) |
| : frame_view_(frame_view), |
| text_paint_timing_detector_( |
| MakeGarbageCollected<TextPaintTimingDetector>(frame_view, |
| this, |
| nullptr /*set later*/)), |
| image_paint_timing_detector_( |
| MakeGarbageCollected<ImagePaintTimingDetector>( |
| frame_view, |
| nullptr /*set later*/)), |
| callback_manager_( |
| MakeGarbageCollected<PaintTimingCallbackManagerImpl>(frame_view)) { |
| if (PaintTimingVisualizer::IsTracingEnabled()) |
| visualizer_.emplace(); |
| text_paint_timing_detector_->ResetCallbackManager(callback_manager_.Get()); |
| image_paint_timing_detector_->ResetCallbackManager(callback_manager_.Get()); |
| } |
| |
| void PaintTimingDetector::NotifyPaintFinished() { |
| if (PaintTimingVisualizer::IsTracingEnabled()) { |
| if (!visualizer_) |
| visualizer_.emplace(); |
| visualizer_->RecordMainFrameViewport(*frame_view_); |
| } else { |
| visualizer_.reset(); |
| } |
| text_paint_timing_detector_->OnPaintFinished(); |
| if (image_paint_timing_detector_) { |
| image_paint_timing_detector_->OnPaintFinished(); |
| if (image_paint_timing_detector_->FinishedReportingImages()) |
| image_paint_timing_detector_ = nullptr; |
| } |
| if (callback_manager_->CountCallbacks() > 0) |
| callback_manager_->RegisterPaintTimeCallbackForCombinedCallbacks(); |
| } |
| |
| // static |
| void PaintTimingDetector::NotifyBackgroundImagePaint( |
| const Node* node, |
| const Image* image, |
| const StyleFetchedImage* style_image, |
| const PropertyTreeState& current_paint_chunk_properties) { |
| DCHECK(image); |
| DCHECK(style_image->CachedImage()); |
| if (!node) |
| return; |
| LayoutObject* object = node->GetLayoutObject(); |
| if (!object) |
| return; |
| LocalFrameView* frame_view = object->GetFrameView(); |
| if (!frame_view) |
| return; |
| PaintTimingDetector& detector = frame_view->GetPaintTimingDetector(); |
| if (!detector.GetImagePaintTimingDetector()) |
| return; |
| if (!IsBackgroundImageContentful(*object, *image)) |
| return; |
| detector.GetImagePaintTimingDetector()->RecordImage( |
| *object, image->Size(), *style_image->CachedImage(), |
| current_paint_chunk_properties, style_image); |
| } |
| |
| // static |
| void PaintTimingDetector::NotifyImagePaint( |
| const LayoutObject& object, |
| const IntSize& intrinsic_size, |
| const ImageResourceContent* cached_image, |
| const PropertyTreeState& current_paint_chunk_properties) { |
| LocalFrameView* frame_view = object.GetFrameView(); |
| if (!frame_view) |
| return; |
| if (!cached_image) |
| return; |
| PaintTimingDetector& detector = frame_view->GetPaintTimingDetector(); |
| if (!detector.GetImagePaintTimingDetector()) |
| return; |
| detector.GetImagePaintTimingDetector()->RecordImage( |
| object, intrinsic_size, *cached_image, current_paint_chunk_properties, |
| nullptr); |
| } |
| |
| void PaintTimingDetector::NotifyImageFinished( |
| const LayoutObject& object, |
| const ImageResourceContent* cached_image) { |
| if (image_paint_timing_detector_) |
| image_paint_timing_detector_->NotifyImageFinished(object, cached_image); |
| } |
| |
| void PaintTimingDetector::LayoutObjectWillBeDestroyed( |
| const LayoutObject& object) { |
| text_paint_timing_detector_->LayoutObjectWillBeDestroyed(object); |
| |
| if (image_paint_timing_detector_) |
| image_paint_timing_detector_->LayoutObjectWillBeDestroyed(object); |
| } |
| |
| void PaintTimingDetector::NotifyImageRemoved( |
| const LayoutObject& object, |
| const ImageResourceContent* cached_image) { |
| if (image_paint_timing_detector_) { |
| image_paint_timing_detector_->NotifyImageRemoved(object, cached_image); |
| } |
| } |
| |
| void PaintTimingDetector::StopRecordingLargestContentfulPaint() { |
| DCHECK(frame_view_); |
| // TextPaintTimingDetector is used for both Largest Contentful Paint and for |
| // Element Timing. Therefore, here we only want to stop recording Largest |
| // Contentful Paint. |
| text_paint_timing_detector_->StopRecordingLargestTextPaint(); |
| // ImagePaintTimingDetector is currently only being used for |
| // LargestContentfulPaint. |
| if (image_paint_timing_detector_) |
| image_paint_timing_detector_->StopRecordEntries(); |
| largest_contentful_paint_calculator_ = nullptr; |
| } |
| |
| void PaintTimingDetector::NotifyInputEvent(WebInputEvent::Type type) { |
| if (type == WebInputEvent::kMouseMove || type == WebInputEvent::kMouseEnter || |
| type == WebInputEvent::kMouseLeave || |
| WebInputEvent::IsPinchGestureEventType(type)) { |
| return; |
| } |
| StopRecordingLargestContentfulPaint(); |
| } |
| |
| void PaintTimingDetector::NotifyScroll(ScrollType scroll_type) { |
| if (scroll_type != kUserScroll && scroll_type != kCompositorScroll) |
| return; |
| StopRecordingLargestContentfulPaint(); |
| } |
| |
| bool PaintTimingDetector::NeedToNotifyInputOrScroll() const { |
| DCHECK(text_paint_timing_detector_); |
| return text_paint_timing_detector_->IsRecordingLargestTextPaint() || |
| (image_paint_timing_detector_ && |
| image_paint_timing_detector_->IsRecording()); |
| } |
| |
| LargestContentfulPaintCalculator* |
| PaintTimingDetector::GetLargestContentfulPaintCalculator() { |
| if (largest_contentful_paint_calculator_) |
| return largest_contentful_paint_calculator_; |
| |
| auto* dom_window = frame_view_->GetFrame().DomWindow(); |
| if (!dom_window) |
| return nullptr; |
| |
| largest_contentful_paint_calculator_ = |
| MakeGarbageCollected<LargestContentfulPaintCalculator>( |
| DOMWindowPerformance::performance(*dom_window)); |
| return largest_contentful_paint_calculator_; |
| } |
| |
| bool PaintTimingDetector::NotifyIfChangedLargestImagePaint( |
| base::TimeTicks image_paint_time, |
| uint64_t image_paint_size) { |
| if (!HasLargestImagePaintChanged(image_paint_time, image_paint_size)) |
| return false; |
| largest_image_paint_time_ = image_paint_time; |
| largest_image_paint_size_ = image_paint_size; |
| DidChangePerformanceTiming(); |
| return true; |
| } |
| |
| bool PaintTimingDetector::NotifyIfChangedLargestTextPaint( |
| base::TimeTicks text_paint_time, |
| uint64_t text_paint_size) { |
| if (!HasLargestTextPaintChanged(text_paint_time, text_paint_size)) |
| return false; |
| largest_text_paint_time_ = text_paint_time; |
| largest_text_paint_size_ = text_paint_size; |
| DidChangePerformanceTiming(); |
| return true; |
| } |
| |
| bool PaintTimingDetector::HasLargestImagePaintChanged( |
| base::TimeTicks largest_image_paint_time, |
| uint64_t largest_image_paint_size) const { |
| return largest_image_paint_time != largest_image_paint_time_ || |
| largest_image_paint_size != largest_image_paint_size_; |
| } |
| |
| bool PaintTimingDetector::HasLargestTextPaintChanged( |
| base::TimeTicks largest_text_paint_time, |
| uint64_t largest_text_paint_size) const { |
| return largest_text_paint_time != largest_text_paint_time_ || |
| largest_text_paint_size != largest_text_paint_size_; |
| } |
| |
| void PaintTimingDetector::DidChangePerformanceTiming() { |
| Document* document = frame_view_->GetFrame().GetDocument(); |
| if (!document) |
| return; |
| DocumentLoader* loader = document->Loader(); |
| if (!loader) |
| return; |
| loader->DidChangePerformanceTiming(); |
| } |
| |
| void PaintTimingDetector::ConvertViewportToWindow( |
| WebFloatRect* float_rect) const { |
| WebLocalFrameImpl* web_frame = |
| WebLocalFrameImpl::FromFrame(&frame_view_->GetFrame()); |
| // May be nullptr in some tests. |
| if (!web_frame) |
| return; |
| WebFrameWidgetBase* widget = web_frame->LocalRootFrameWidget(); |
| DCHECK(widget); |
| widget->Client()->ConvertViewportToWindow(float_rect); |
| } |
| |
| FloatRect PaintTimingDetector::CalculateVisualRect( |
| const IntRect& visual_rect, |
| const PropertyTreeState& current_paint_chunk_properties) const { |
| // This case should be dealt with outside the function. |
| DCHECK(!visual_rect.IsEmpty()); |
| |
| // As Layout objects live in different transform spaces, the object's rect |
| // should be projected to the viewport's transform space. |
| FloatClipRect float_clip_visual_rect = FloatClipRect(FloatRect(visual_rect)); |
| GeometryMapper::LocalToAncestorVisualRect(current_paint_chunk_properties, |
| PropertyTreeState::Root(), |
| float_clip_visual_rect); |
| WebFloatRect float_visual_rect = float_clip_visual_rect.Rect(); |
| if (frame_view_->GetFrame().LocalFrameRoot().IsMainFrame()) { |
| ConvertViewportToWindow(&float_visual_rect); |
| return float_visual_rect; |
| } |
| // OOPIF. The final rect lives in the iframe's root frame space. We need to |
| // project it to the top frame space. |
| auto layout_visual_rect = PhysicalRect::EnclosingRect(float_visual_rect); |
| frame_view_->GetFrame() |
| .LocalFrameRoot() |
| .View() |
| ->MapToVisualRectInTopFrameSpace(layout_visual_rect); |
| WebFloatRect float_rect = FloatRect(layout_visual_rect); |
| ConvertViewportToWindow(&float_rect); |
| return float_rect; |
| } |
| |
| void PaintTimingDetector::UpdateLargestContentfulPaintCandidate() { |
| auto* lcp_calculator = GetLargestContentfulPaintCalculator(); |
| if (!lcp_calculator) |
| return; |
| |
| // Optional, WeakPtr, Record have different roles: |
| // * !Optional means |UpdateCandidate() is not reachable, e.g., user input |
| // has been given to stop LCP. In this case, we still use the last recorded |
| // result. |
| // * !Weak means there is no candidate, e.g., no content show up on the page. |
| // * Record.paint_time == 0 means there is an image but the image is still |
| // loading. The perf API should wait until the paint-time is available. |
| base::Optional<base::WeakPtr<TextRecord>> largest_text_record; |
| base::Optional<const ImageRecord*> largest_image_record; |
| if (auto* text_timing_detector = GetTextPaintTimingDetector()) { |
| if (text_timing_detector->IsRecordingLargestTextPaint()) { |
| largest_text_record.emplace(text_timing_detector->UpdateCandidate()); |
| } |
| } |
| if (auto* image_timing_detector = GetImagePaintTimingDetector()) { |
| largest_image_record.emplace(image_timing_detector->UpdateCandidate()); |
| } |
| |
| lcp_calculator->UpdateLargestContentPaintIfNeeded(largest_text_record, |
| largest_image_record); |
| } |
| |
| ScopedPaintTimingDetectorBlockPaintHook* |
| ScopedPaintTimingDetectorBlockPaintHook::top_ = nullptr; |
| |
| void ScopedPaintTimingDetectorBlockPaintHook::EmplaceIfNeeded( |
| const LayoutBoxModelObject& aggregator, |
| const PropertyTreeState& property_tree_state) { |
| // |reset_top_| is unset when |aggregator| is anonymous so that each |
| // aggregation corresponds to an element. See crbug.com/988593. When set, |
| // |top_| becomes |this|, and |top_| is restored to the previous value when |
| // the ScopedPaintTimingDetectorBlockPaintHook goes out of scope. |
| if (aggregator.GetNode()) |
| reset_top_.emplace(&top_, this); |
| |
| TextPaintTimingDetector* detector = aggregator.GetFrameView() |
| ->GetPaintTimingDetector() |
| .GetTextPaintTimingDetector(); |
| // Only set |data_| if we need to walk the object. |
| if (detector && detector->ShouldWalkObject(aggregator)) |
| data_.emplace(aggregator, property_tree_state, detector); |
| } |
| |
| ScopedPaintTimingDetectorBlockPaintHook::Data::Data( |
| const LayoutBoxModelObject& aggregator, |
| const PropertyTreeState& property_tree_state, |
| TextPaintTimingDetector* detector) |
| : aggregator_(aggregator), |
| property_tree_state_(property_tree_state), |
| detector_(detector) {} |
| |
| ScopedPaintTimingDetectorBlockPaintHook:: |
| ~ScopedPaintTimingDetectorBlockPaintHook() { |
| if (!data_ || data_->aggregated_visual_rect_.IsEmpty()) |
| return; |
| // TODO(crbug.com/987804): Checking |ShouldWalkObject| again is necessary |
| // because the result can change, but more investigation is needed as to why |
| // the change is possible. |
| if (!data_->detector_ || |
| !data_->detector_->ShouldWalkObject(data_->aggregator_)) |
| return; |
| data_->detector_->RecordAggregatedText(data_->aggregator_, |
| data_->aggregated_visual_rect_, |
| data_->property_tree_state_); |
| } |
| |
| void PaintTimingDetector::Trace(Visitor* visitor) { |
| visitor->Trace(text_paint_timing_detector_); |
| visitor->Trace(image_paint_timing_detector_); |
| visitor->Trace(frame_view_); |
| visitor->Trace(largest_contentful_paint_calculator_); |
| visitor->Trace(callback_manager_); |
| visitor->Trace(visualizer_); |
| } |
| |
| void PaintTimingCallbackManagerImpl:: |
| RegisterPaintTimeCallbackForCombinedCallbacks() { |
| DCHECK(!frame_callbacks_->empty()); |
| LocalFrame& frame = frame_view_->GetFrame(); |
| if (!frame.GetPage()) |
| return; |
| |
| auto combined_callback = CrossThreadBindOnce( |
| &PaintTimingCallbackManagerImpl::ReportPaintTime, |
| WrapCrossThreadWeakPersistent(this), std::move(frame_callbacks_)); |
| frame_callbacks_ = |
| std::make_unique<PaintTimingCallbackManager::CallbackQueue>(); |
| |
| // |ReportPaintTime| on |layerTreeView| will queue a swap-promise, the |
| // callback is called when the swap for current render frame completes or |
| // fails to happen. |
| frame.GetPage()->GetChromeClient().NotifySwapTime( |
| frame, std::move(combined_callback)); |
| } |
| |
| void PaintTimingCallbackManagerImpl::ReportPaintTime( |
| std::unique_ptr<PaintTimingCallbackManager::CallbackQueue> frame_callbacks, |
| WebWidgetClient::SwapResult result, |
| base::TimeTicks paint_time) { |
| while (!frame_callbacks->empty()) { |
| std::move(frame_callbacks->front()).Run(paint_time); |
| frame_callbacks->pop(); |
| } |
| frame_view_->GetPaintTimingDetector().UpdateLargestContentfulPaintCandidate(); |
| } |
| |
| void PaintTimingCallbackManagerImpl::Trace(Visitor* visitor) { |
| visitor->Trace(frame_view_); |
| PaintTimingCallbackManager::Trace(visitor); |
| } |
| |
| } // namespace blink |