| // Copyright 2016 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/timing/window_performance.h" |
| |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/bindings/core/v8/string_or_double.h" |
| #include "third_party/blink/renderer/bindings/core/v8/string_or_double_or_performance_measure_options.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" |
| #include "third_party/blink/renderer/core/dom/document_init.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/performance_monitor.h" |
| #include "third_party/blink/renderer/core/loader/document_load_timing.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/core/testing/dummy_page_holder.h" |
| #include "third_party/blink/renderer/core/timing/dom_window_performance.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/wtf/scoped_mock_clock.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| TimeTicks GetTimeOrigin() { |
| return TimeTicks() + TimeDelta::FromSeconds(500); |
| } |
| |
| } // namespace |
| |
| class WindowPerformanceTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| ResetPerformance(); |
| |
| // Create another dummy page holder and pretend this is the iframe. |
| another_page_holder_ = DummyPageHolder::Create(IntSize(400, 300)); |
| another_page_holder_->GetDocument().SetURL(KURL("https://iframed.com/bar")); |
| } |
| |
| bool ObservingLongTasks() { |
| return PerformanceMonitor::InstrumentingMonitor( |
| performance_->GetExecutionContext()); |
| } |
| |
| void AddLongTaskObserver() { |
| // simulate with filter options. |
| performance_->observer_filter_options_ |= PerformanceEntry::kLongTask; |
| } |
| |
| void RemoveLongTaskObserver() { |
| // simulate with filter options. |
| performance_->observer_filter_options_ = PerformanceEntry::kInvalid; |
| } |
| |
| void SimulateDidProcessLongTask() { |
| auto* monitor = GetFrame()->GetPerformanceMonitor(); |
| monitor->WillExecuteScript(GetDocument()); |
| monitor->DidExecuteScript(); |
| monitor->DidProcessTask( |
| base::TimeTicks(), base::TimeTicks() + base::TimeDelta::FromSeconds(1)); |
| } |
| |
| void SimulateSwapPromise(TimeTicks timestamp) { |
| performance_->ReportEventTimings(WebLayerTreeView::SwapResult::kDidSwap, |
| timestamp); |
| } |
| |
| LocalFrame* GetFrame() const { return &page_holder_->GetFrame(); } |
| |
| Document* GetDocument() const { return &page_holder_->GetDocument(); } |
| |
| LocalFrame* AnotherFrame() const { return &another_page_holder_->GetFrame(); } |
| |
| Document* AnotherDocument() const { |
| return &another_page_holder_->GetDocument(); |
| } |
| |
| String SanitizedAttribution(ExecutionContext* context, |
| bool has_multiple_contexts, |
| LocalFrame* observer_frame) { |
| return WindowPerformance::SanitizedAttribution( |
| context, has_multiple_contexts, observer_frame) |
| .first; |
| } |
| |
| void ResetPerformance() { |
| page_holder_ = DummyPageHolder::Create(IntSize(800, 600)); |
| page_holder_->GetDocument().SetURL(KURL("https://example.com")); |
| performance_ = |
| WindowPerformance::Create(page_holder_->GetDocument().domWindow()); |
| performance_->time_origin_ = GetTimeOrigin(); |
| } |
| |
| Persistent<WindowPerformance> performance_; |
| std::unique_ptr<DummyPageHolder> page_holder_; |
| std::unique_ptr<DummyPageHolder> another_page_holder_; |
| }; |
| |
| TEST_F(WindowPerformanceTest, LongTaskObserverInstrumentation) { |
| performance_->UpdateLongTaskInstrumentation(); |
| EXPECT_FALSE(ObservingLongTasks()); |
| |
| // Adding LongTask observer (with filer option) enables instrumentation. |
| AddLongTaskObserver(); |
| performance_->UpdateLongTaskInstrumentation(); |
| EXPECT_TRUE(ObservingLongTasks()); |
| |
| // Removing LongTask observer disables instrumentation. |
| RemoveLongTaskObserver(); |
| performance_->UpdateLongTaskInstrumentation(); |
| EXPECT_FALSE(ObservingLongTasks()); |
| } |
| |
| TEST_F(WindowPerformanceTest, SanitizedLongTaskName) { |
| // Unable to attribute, when no execution contents are available. |
| EXPECT_EQ("unknown", SanitizedAttribution(nullptr, false, GetFrame())); |
| |
| // Attribute for same context (and same origin). |
| EXPECT_EQ("self", SanitizedAttribution(GetDocument(), false, GetFrame())); |
| |
| // Unable to attribute, when multiple script execution contents are involved. |
| EXPECT_EQ("multiple-contexts", |
| SanitizedAttribution(GetDocument(), true, GetFrame())); |
| } |
| |
| TEST_F(WindowPerformanceTest, SanitizedLongTaskName_CrossOrigin) { |
| // Unable to attribute, when no execution contents are available. |
| EXPECT_EQ("unknown", SanitizedAttribution(nullptr, false, GetFrame())); |
| |
| // Attribute for same context (and same origin). |
| EXPECT_EQ("cross-origin-unreachable", |
| SanitizedAttribution(AnotherDocument(), false, GetFrame())); |
| } |
| |
| // https://crbug.com/706798: Checks that after navigation that have replaced the |
| // window object, calls to not garbage collected yet WindowPerformance belonging |
| // to the old window do not cause a crash. |
| TEST_F(WindowPerformanceTest, NavigateAway) { |
| AddLongTaskObserver(); |
| performance_->UpdateLongTaskInstrumentation(); |
| EXPECT_TRUE(ObservingLongTasks()); |
| |
| // Simulate navigation commit. |
| DocumentInit init = DocumentInit::Create().WithDocumentLoader( |
| GetFrame()->Loader().GetDocumentLoader()); |
| GetDocument()->Shutdown(); |
| GetFrame()->SetDOMWindow(LocalDOMWindow::Create(*GetFrame())); |
| GetFrame()->DomWindow()->InstallNewDocument(AtomicString(), init, false); |
| |
| // m_performance is still alive, and should not crash when notified. |
| SimulateDidProcessLongTask(); |
| } |
| |
| // Checks that WindowPerformance object and its fields (like PerformanceTiming) |
| // function correctly after transition to another document in the same window. |
| // This happens when a page opens a new window and it navigates to a same-origin |
| // document. |
| TEST(PerformanceLifetimeTest, SurviveContextSwitch) { |
| std::unique_ptr<DummyPageHolder> page_holder = |
| DummyPageHolder::Create(IntSize(800, 600)); |
| |
| WindowPerformance* perf = |
| DOMWindowPerformance::performance(*page_holder->GetFrame().DomWindow()); |
| PerformanceTiming* timing = perf->timing(); |
| |
| auto* document_loader = page_holder->GetFrame().Loader().GetDocumentLoader(); |
| ASSERT_TRUE(document_loader); |
| document_loader->GetTiming().SetNavigationStart(CurrentTimeTicks()); |
| |
| EXPECT_EQ(&page_holder->GetFrame(), perf->GetFrame()); |
| EXPECT_EQ(&page_holder->GetFrame(), timing->GetFrame()); |
| auto navigation_start = timing->navigationStart(); |
| EXPECT_NE(0U, navigation_start); |
| |
| // Simulate changing the document while keeping the window. |
| page_holder->GetDocument().Shutdown(); |
| page_holder->GetFrame().DomWindow()->InstallNewDocument( |
| AtomicString(), |
| DocumentInit::Create().WithDocumentLoader(document_loader), false); |
| |
| EXPECT_EQ(perf, DOMWindowPerformance::performance( |
| *page_holder->GetFrame().DomWindow())); |
| EXPECT_EQ(timing, perf->timing()); |
| EXPECT_EQ(&page_holder->GetFrame(), perf->GetFrame()); |
| EXPECT_EQ(&page_holder->GetFrame(), timing->GetFrame()); |
| EXPECT_EQ(navigation_start, timing->navigationStart()); |
| } |
| |
| // Make sure the output entries with the same timestamps follow the insertion |
| // order. (http://crbug.com/767560) |
| TEST_F(WindowPerformanceTest, EnsureEntryListOrder) { |
| V8TestingScope scope; |
| WTF::ScopedMockClock clock; |
| clock.Advance(GetTimeOrigin() - TimeTicks()); |
| |
| DummyExceptionStateForTesting exception_state; |
| clock.Advance(TimeDelta::FromSeconds(2)); |
| for (int i = 0; i < 8; i++) { |
| performance_->mark(scope.GetScriptState(), AtomicString::Number(i), |
| exception_state); |
| } |
| clock.Advance(TimeDelta::FromSeconds(2)); |
| for (int i = 8; i < 17; i++) { |
| performance_->mark(scope.GetScriptState(), AtomicString::Number(i), |
| exception_state); |
| } |
| PerformanceEntryVector entries = performance_->getEntries(); |
| EXPECT_EQ(17U, entries.size()); |
| for (int i = 0; i < 8; i++) { |
| EXPECT_EQ(AtomicString::Number(i), entries[i]->name()); |
| EXPECT_NEAR(2000, entries[i]->startTime(), 0.005); |
| } |
| for (int i = 8; i < 17; i++) { |
| EXPECT_EQ(AtomicString::Number(i), entries[i]->name()); |
| EXPECT_NEAR(4000, entries[i]->startTime(), 0.005); |
| } |
| } |
| |
| TEST_F(WindowPerformanceTest, EventTimingBeforeOnLoad) { |
| ScopedEventTimingForTest event_timing(true); |
| EXPECT_TRUE(page_holder_->GetFrame().Loader().GetDocumentLoader()); |
| |
| TimeTicks start_time = GetTimeOrigin() + TimeDelta::FromSecondsD(1.1); |
| TimeTicks processing_start = GetTimeOrigin() + TimeDelta::FromSecondsD(3.3); |
| TimeTicks processing_end = GetTimeOrigin() + TimeDelta::FromSecondsD(3.8); |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, false); |
| TimeTicks swap_time = GetTimeOrigin() + TimeDelta::FromSecondsD(6.0); |
| SimulateSwapPromise(swap_time); |
| EXPECT_EQ(1u, performance_->getEntriesByName("click", "event").size()); |
| performance_->clearEventTimings(); |
| |
| page_holder_->GetFrame() |
| .Loader() |
| .GetDocumentLoader() |
| ->GetTiming() |
| .MarkLoadEventStart(); |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, true); |
| SimulateSwapPromise(swap_time); |
| EXPECT_EQ(0u, performance_->getEntriesByName("click", "event").size()); |
| performance_->clearEventTimings(); |
| |
| EXPECT_TRUE(page_holder_->GetFrame().Loader().GetDocumentLoader()); |
| GetFrame()->PrepareForCommit(); |
| EXPECT_FALSE(page_holder_->GetFrame().Loader().GetDocumentLoader()); |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, false); |
| SimulateSwapPromise(swap_time); |
| EXPECT_EQ(1u, performance_->getEntriesByName("click", "event").size()); |
| performance_->clearEventTimings(); |
| } |
| |
| TEST_F(WindowPerformanceTest, EventTimingDuration) { |
| ScopedEventTimingForTest event_timing(true); |
| |
| TimeTicks start_time = GetTimeOrigin() + TimeDelta::FromMilliseconds(1000); |
| TimeTicks processing_start = |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(1001); |
| TimeTicks processing_end = |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(1002); |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, false); |
| TimeTicks short_swap_time = |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(1003); |
| SimulateSwapPromise(short_swap_time); |
| EXPECT_EQ(0u, performance_->getEntriesByName("click", "event").size()); |
| |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, true); |
| TimeTicks long_swap_time = |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(1100); |
| SimulateSwapPromise(long_swap_time); |
| EXPECT_EQ(1u, performance_->getEntriesByName("click", "event").size()); |
| |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, true); |
| SimulateSwapPromise(short_swap_time); |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, false); |
| SimulateSwapPromise(long_swap_time); |
| EXPECT_EQ(2u, performance_->getEntriesByName("click", "event").size()); |
| } |
| |
| TEST_F(WindowPerformanceTest, MultipleEventsSameSwap) { |
| ScopedEventTimingForTest event_timing(true); |
| |
| size_t num_events = 10; |
| for (size_t i = 0; i < num_events; ++i) { |
| TimeTicks start_time = GetTimeOrigin() + TimeDelta::FromSeconds(i); |
| TimeTicks processing_start = start_time + TimeDelta::FromMilliseconds(100); |
| TimeTicks processing_end = start_time + TimeDelta::FromMilliseconds(200); |
| performance_->RegisterEventTiming("click", start_time, processing_start, |
| processing_end, false); |
| EXPECT_EQ(0u, performance_->getEntriesByName("click", "event").size()); |
| } |
| TimeTicks swap_time = GetTimeOrigin() + TimeDelta::FromSeconds(num_events); |
| SimulateSwapPromise(swap_time); |
| EXPECT_EQ(num_events, |
| performance_->getEntriesByName("click", "event").size()); |
| } |
| |
| // Test for existence of 'firstInput' given different types of first events. |
| TEST_F(WindowPerformanceTest, FirstInput) { |
| struct { |
| AtomicString event_type; |
| bool should_report; |
| } inputs[] = {{"click", true}, {"keydown", true}, |
| {"keypress", false}, {"pointerdown", false}, |
| {"mousedown", true}, {"mousemove", false}, |
| {"mouseover", false}}; |
| for (const auto& input : inputs) { |
| // firstInput does not have a |duration| threshold so use close values. |
| performance_->RegisterEventTiming( |
| input.event_type, GetTimeOrigin(), |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(1), |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(2), false); |
| SimulateSwapPromise(GetTimeOrigin() + TimeDelta::FromMilliseconds(3)); |
| PerformanceEntryVector firstInputs = |
| performance_->getEntriesByType("firstInput"); |
| EXPECT_GE(1u, firstInputs.size()); |
| EXPECT_EQ(input.should_report, firstInputs.size() == 1u); |
| ResetPerformance(); |
| } |
| } |
| |
| // Test that the 'firstInput' is populated after some irrelevant events are |
| // ignored. |
| TEST_F(WindowPerformanceTest, FirstInputAfterIgnored) { |
| AtomicString several_events[] = {"mousemove", "mouseover", "mousedown"}; |
| for (const auto& event : several_events) { |
| performance_->RegisterEventTiming( |
| event, GetTimeOrigin(), |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(1), |
| GetTimeOrigin() + TimeDelta::FromMilliseconds(2), false); |
| } |
| SimulateSwapPromise(GetTimeOrigin() + TimeDelta::FromMilliseconds(3)); |
| ASSERT_EQ(1u, performance_->getEntriesByType("firstInput").size()); |
| EXPECT_EQ("mousedown", |
| performance_->getEntriesByType("firstInput")[0]->name()); |
| } |
| |
| // Test that pointerdown followed by pointerup works as a 'firstInput'. |
| TEST_F(WindowPerformanceTest, FirstPointerUp) { |
| TimeTicks start_time = GetTimeOrigin(); |
| TimeTicks processing_start = GetTimeOrigin() + TimeDelta::FromMilliseconds(1); |
| TimeTicks processing_end = GetTimeOrigin() + TimeDelta::FromMilliseconds(2); |
| TimeTicks swap_time = GetTimeOrigin() + TimeDelta::FromMilliseconds(3); |
| performance_->RegisterEventTiming("pointerdown", start_time, processing_start, |
| processing_end, false); |
| SimulateSwapPromise(swap_time); |
| EXPECT_EQ(0u, performance_->getEntriesByType("firstInput").size()); |
| performance_->RegisterEventTiming("pointerup", start_time, processing_start, |
| processing_end, false); |
| SimulateSwapPromise(swap_time); |
| EXPECT_EQ(1u, performance_->getEntriesByType("firstInput").size()); |
| // The name of the entry should be "pointerdown". |
| EXPECT_EQ(1u, |
| performance_->getEntriesByName("pointerdown", "firstInput").size()); |
| } |
| |
| } // namespace blink |