Commit fb034084 authored by Nicolas Pena's avatar Nicolas Pena Committed by Commit Bot

EventTiming: Implement First Input

This CL implements the PerformanceEventTiming entry of type "firstInput"
whose explanation can be found at:
https://github.com/WICG/event-timing#first-input-timing

Bug: 841224, 851484
Change-Id: I40d799f1920ec2fc5fb9140a5e5faec6642a7f03
Reviewed-on: https://chromium-review.googlesource.com/1103230Reviewed-by: default avatarDave Tapuska <dtapuska@chromium.org>
Reviewed-by: default avatarTimothy Dresser <tdresser@chromium.org>
Commit-Queue: Nicolás Peña Moreno <npm@chromium.org>
Cr-Commit-Position: refs/heads/master@{#569062}
parent 528cf76e
......@@ -23,7 +23,7 @@
}
function validateEntries() {
const entries = performance.getEntriesByName('click');
const entries = performance.getEntriesByName('click', 'event');
const onloadTime = performance.timing.loadEventStart - performance.timeOrigin;
const entriesBeforeOnload = entries.filter(
......@@ -31,7 +31,7 @@
assert_equals(entriesBeforeOnload.length, 1,
"Long latency events before onload should be buffered.");
const entry = entriesBeforeOnload[0];
verifyClickEvent(entry);
verifyClickEvent(entry, true);
assert_greater_than(entry.processingStart, processingStartMin,
"The entry should be processed later than processingStartMin.");
......
......@@ -21,14 +21,14 @@
let processingStartMin;
function validateEntries() {
const entries = performance.getEntriesByName('click');
const entries = performance.getEntriesByName('click', 'event');
const onloadTime = performance.timing.loadEventStart - performance.timeOrigin;
assert_equals(entries.length, 1,
"Observer of main frames should only capture main-frame event-timing entries."
);
const entry = entries[0];
verifyClickEvent(entry);
verifyClickEvent(entry, true);
assert_greater_than(entry.processingStart, processingStartMin,
"The entry's processing start should be later than processingStartMin.");
......
......@@ -23,7 +23,7 @@
"onload should be later than entry's start time.");
assert_greater_than(entry.processingStart, timeAfterFirstClick,
"The entry's processing start should be later than timeAfterFirstClick.");
verifyClickEvent(entry);
verifyClickEvent(entry, true);
}
function verifyObserverEntries(observedEntries) {
......@@ -79,7 +79,7 @@
const bufferPromise = clickAndBlockMain('button').then(wait);
Promise.all([observerPromise, bufferPromise]).then((results) => {
t.step(verifyObserverEntries.bind(null, results[0]));
t.step(verifyBuffer.bind(null, performance.getEntriesByName('click')));
t.step(verifyBuffer.bind(null, performance.getEntriesByName('click', 'event')));
t.done();
});
});
......
......@@ -15,7 +15,7 @@ registration are lost
function verifyBufferAndObserverEntries(observedEntries) {
// Verify buffer entries
const bufferedEntries = performance.getEntriesByName('click');
const bufferedEntries = performance.getEntriesByName('click', 'event');
const bufferedEntriesBeforeObserver = bufferedEntries.filter(e => e.startTime <
observerStart);
assert_equals(bufferedEntries.length, 0,
......
......@@ -10,7 +10,7 @@
<script>
function validateEntries() {
const entriesByName = performance.getEntriesByName('click');
const entriesByName = performance.getEntriesByName('click', 'event');
const entriesByType = performance.getEntriesByType('event');
const allEntries = performance.getEntries();
assert_equals(entriesByName.length, 1, 'event-timing entry should be retrievable by getEntriesByName');
......
......@@ -21,7 +21,10 @@ function mainThreadBusy(duration) {
while (performance.now() < now + duration);
}
function verifyClickEvent(entry) {
// This method should receive an entry of type 'event'. |is_false| is true only
// when the event also happens to correspond to the first event. In this case,
// the timings of the 'firstInput' entry should be equal to those of this entry.
function verifyClickEvent(entry, is_first=false) {
assert_true(entry.cancelable);
assert_equals(entry.name, 'click');
assert_equals(entry.entryType, 'event');
......@@ -33,6 +36,18 @@ function verifyClickEvent(entry) {
"The entry's processingEnd must be at least as large as processingStart.");
assert_greater_than_equal(entry.duration, entry.processingEnd - entry.startTime,
"The entry's duration must be at least as large as processingEnd - startTime.");
if (is_first) {
let firstInputs = performance.getEntriesByType('firstInput');
assert_equals(firstInputs.length, 1, 'There should be a single firstInput entry');
let firstInput = firstInputs[0];
assert_equals(firstInput.name, entry.name);
assert_equals(firstInput.entryType, 'firstInput');
assert_equals(firstInput.startTime, entry.startTime);
assert_equals(firstInput.duration, entry.duration);
assert_equals(firstInput.processingStart, entry.processingStart);
assert_equals(firstInput.processingEnd, entry.processingEnd);
assert_equals(firstInput.cancelable, entry.cancelable);
}
}
function wait() {
......
......@@ -135,6 +135,8 @@ PerformanceEntryVector Performance::getEntries() {
entries.AppendVector(resource_timing_buffer_);
entries.AppendVector(event_timing_buffer_);
if (first_input_timing_)
entries.push_back(first_input_timing_);
if (!navigation_timing_)
navigation_timing_ = CreateNavigationTimingInstance();
// This extra checking is needed when WorkerPerformance
......@@ -172,6 +174,10 @@ PerformanceEntryVector Performance::getEntriesByType(const String& entry_type) {
for (const auto& event : event_timing_buffer_)
entries.push_back(event);
break;
case PerformanceEntry::kFirstInput:
if (first_input_timing_)
entries.push_back(first_input_timing_);
break;
case PerformanceEntry::kNavigation:
if (!navigation_timing_)
navigation_timing_ = CreateNavigationTimingInstance();
......@@ -241,6 +247,11 @@ PerformanceEntryVector Performance::getEntriesByName(const String& name,
}
}
if (entry_type.IsNull() || type == PerformanceEntry::kFirstInput) {
if (first_input_timing_ && first_input_timing_->name() == name)
entries.push_back(first_input_timing_);
}
if (entry_type.IsNull() || type == PerformanceEntry::kNavigation) {
if (!navigation_timing_)
navigation_timing_ = CreateNavigationTimingInstance();
......@@ -870,6 +881,7 @@ void Performance::Trace(blink::Visitor* visitor) {
visitor->Trace(user_timing_);
visitor->Trace(first_paint_timing_);
visitor->Trace(first_contentful_paint_timing_);
visitor->Trace(first_input_timing_);
visitor->Trace(observers_);
visitor->Trace(active_observers_);
visitor->Trace(suspended_observers_);
......
......@@ -258,6 +258,7 @@ class CORE_EXPORT Performance : public EventTargetWithInlineData {
Member<UserTiming> user_timing_;
Member<PerformanceEntry> first_paint_timing_;
Member<PerformanceEntry> first_contentful_paint_timing_;
Member<PerformanceEventTiming> first_input_timing_;
TimeTicks time_origin_;
......
......@@ -91,6 +91,8 @@ PerformanceEntry::EntryType PerformanceEntry::ToEntryTypeEnum(
return kPaint;
if (entry_type == "event")
return kEvent;
if (entry_type == "firstInput")
return kFirstInput;
return kInvalid;
}
......
......@@ -64,7 +64,8 @@ class CORE_EXPORT PerformanceEntry : public ScriptWrappable {
kLongTask = 1 << 6,
kTaskAttribution = 1 << 7,
kPaint = 1 << 8,
kEvent = 1 << 9
kEvent = 1 << 9,
kFirstInput = 1 << 10,
};
String name() const;
......
......@@ -10,7 +10,7 @@ namespace blink {
// static
PerformanceEventTiming* PerformanceEventTiming::Create(
const String& type,
const String& event_type,
DOMHighResTimeStamp start_time,
DOMHighResTimeStamp processing_start,
DOMHighResTimeStamp processing_end,
......@@ -18,17 +18,29 @@ PerformanceEventTiming* PerformanceEventTiming::Create(
// TODO(npm): enable this DCHECK once https://crbug.com/852846 is fixed.
// DCHECK_LE(start_time, processing_start);
DCHECK_LE(processing_start, processing_end);
return new PerformanceEventTiming(type, start_time, processing_start,
processing_end, cancelable);
return new PerformanceEventTiming(event_type, "event", start_time,
processing_start, processing_end,
cancelable);
}
// static
PerformanceEventTiming* PerformanceEventTiming::CreateFirstInputTiming(
PerformanceEventTiming* entry) {
PerformanceEventTiming* first_input = new PerformanceEventTiming(
entry->name(), "firstInput", entry->startTime(), entry->processingStart(),
entry->processingEnd(), entry->cancelable());
first_input->SetDuration(entry->duration());
return first_input;
}
PerformanceEventTiming::PerformanceEventTiming(
const String& type,
const String& event_type,
const String& entry_type,
DOMHighResTimeStamp start_time,
DOMHighResTimeStamp processing_start,
DOMHighResTimeStamp processing_end,
bool cancelable)
: PerformanceEntry(type, "event", start_time, 0.0),
: PerformanceEntry(event_type, entry_type, start_time, 0.0),
processing_start_(processing_start),
processing_end_(processing_end),
cancelable_(cancelable) {}
......@@ -44,7 +56,8 @@ DOMHighResTimeStamp PerformanceEventTiming::processingEnd() const {
}
void PerformanceEventTiming::SetDuration(double duration) {
DCHECK_LE(0, duration);
// TODO(npm): enable this DCHECK once https://crbug.com/852846 is fixed.
// DCHECK_LE(0, duration);
duration_ = duration;
}
......
......@@ -16,12 +16,15 @@ class CORE_EXPORT PerformanceEventTiming final : public PerformanceEntry {
DEFINE_WRAPPERTYPEINFO();
public:
static PerformanceEventTiming* Create(const String& type,
static PerformanceEventTiming* Create(const String& event_type,
DOMHighResTimeStamp start_time,
DOMHighResTimeStamp processing_start,
DOMHighResTimeStamp processing_end,
bool cancelable);
static PerformanceEventTiming* CreateFirstInputTiming(
PerformanceEventTiming* entry);
~PerformanceEventTiming() override;
bool cancelable() const { return cancelable_; }
......@@ -36,7 +39,8 @@ class CORE_EXPORT PerformanceEventTiming final : public PerformanceEntry {
void Trace(blink::Visitor*) override;
private:
PerformanceEventTiming(const String& type,
PerformanceEventTiming(const String& event_type,
const String& entry_type,
DOMHighResTimeStamp start_time,
DOMHighResTimeStamp processing_start,
DOMHighResTimeStamp processing_end,
......
......@@ -207,6 +207,7 @@ void WindowPerformance::BuildJSONValue(V8ObjectBuilder& builder) const {
void WindowPerformance::Trace(blink::Visitor* visitor) {
visitor->Trace(event_timings_);
visitor->Trace(first_pointer_down_event_timing_);
visitor->Trace(navigation_);
visitor->Trace(timing_);
Performance::Trace(visitor);
......@@ -349,13 +350,27 @@ void WindowPerformance::RegisterEventTiming(String event_type,
void WindowPerformance::ReportEventTimings(WebLayerTreeView::SwapResult result,
TimeTicks timestamp) {
DCHECK(OriginTrials::eventTimingEnabled(GetExecutionContext()));
DOMHighResTimeStamp end_time = MonotonicTimeToDOMHighResTimeStamp(timestamp);
for (const auto& entry : event_timings_) {
int duration_in_ms = std::ceil((end_time - entry->startTime()) / 8) * 8;
entry->SetDuration(duration_in_ms);
if (!first_input_timing_) {
if (entry->name() == "pointerdown") {
first_pointer_down_event_timing_ =
PerformanceEventTiming::CreateFirstInputTiming(entry);
} else if (entry->name() == "pointerup") {
DispatchFirstInputTiming(first_pointer_down_event_timing_);
} else if (entry->name() == "click" || entry->name() == "keydown" ||
entry->name() == "mousedown") {
DispatchFirstInputTiming(
PerformanceEventTiming::CreateFirstInputTiming(entry));
}
}
if (duration_in_ms <= kEventTimingDurationThresholdInMs)
continue;
entry->SetDuration(duration_in_ms);
if (ObservingEventTimingEntries())
NotifyObserversOfEntry(*entry);
......@@ -365,4 +380,19 @@ void WindowPerformance::ReportEventTimings(WebLayerTreeView::SwapResult result,
event_timings_.clear();
}
void WindowPerformance::DispatchFirstInputTiming(
PerformanceEventTiming* entry) {
DCHECK(OriginTrials::eventTimingEnabled(GetExecutionContext()));
if (!entry)
return;
DCHECK_EQ("firstInput", entry->entryType());
if (HasObserverFor(PerformanceEntry::kFirstInput))
NotifyObserversOfEntry(*entry);
DCHECK(!first_input_timing_);
if (ShouldBufferEventTiming())
first_input_timing_ = entry;
}
} // namespace blink
......@@ -105,10 +105,13 @@ class CORE_EXPORT WindowPerformance final : public Performance,
void ReportEventTimings(WebLayerTreeView::SwapResult result,
TimeTicks timestamp);
void DispatchFirstInputTiming(PerformanceEventTiming* entry);
// PerformanceEventTiming entries that have not been added yet: the event
// dispatch has been completed but the swap promise used to determine
// |duration| has not been resolved.
HeapVector<Member<PerformanceEventTiming>> event_timings_;
Member<PerformanceEventTiming> first_pointer_down_event_timing_;
mutable Member<PerformanceNavigation> navigation_;
mutable Member<PerformanceTiming> timing_;
};
......
......@@ -31,11 +31,7 @@ TimeTicks GetTimeOrigin() {
class WindowPerformanceTest : public testing::Test {
protected:
void SetUp() override {
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();
ResetPerformance();
// Create another dummy page holder and pretend this is the iframe.
another_page_holder_ = DummyPageHolder::Create(IntSize(400, 300));
......@@ -88,6 +84,14 @@ class WindowPerformanceTest : public testing::Test {
.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_;
......@@ -296,4 +300,63 @@ TEST_F(WindowPerformanceTest, MultipleEventsSameSwap) {
performance_->getEntriesByName("click", "event").size());
}
// Test for existence of 'firstInput' given different types of first events.
TEST_F(WindowPerformanceTest, FirstInput) {
struct {
String 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) {
String 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment