These helped me understand the bugs, and I think they are useful to keep as the spec is changing and we will most likely need to update this code again. Differential Revision: https://phabricator.services.mozilla.com/D288418
287 lines
10 KiB
C++
287 lines
10 KiB
C++
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||
|
||
#include "PerformanceInteractionMetrics.h"
|
||
|
||
#include "mozilla/EventForwards.h"
|
||
#include "mozilla/Logging.h"
|
||
#include "mozilla/Maybe.h"
|
||
#include "mozilla/MouseEvents.h"
|
||
#include "mozilla/RandomNum.h"
|
||
#include "mozilla/TextEvents.h"
|
||
|
||
static mozilla::LazyLogModule gEventTimingLog("EventTiming");
|
||
|
||
// Interaction ID increment. We increase this value by an integer greater than 1
|
||
// to discourage developers from using the value to 'count' the number of user
|
||
// interactions. This is consistent with the spec, which allows the increasing
|
||
// the user interaction value by a small number chosen by the user agent.
|
||
constexpr uint32_t kInteractionIdIncrement = 7;
|
||
// Minimum potential value for the first Interaction ID.
|
||
constexpr uint32_t kMinFirstInteractionID = 100;
|
||
// Maximum potential value for the first Interaction ID.
|
||
constexpr uint32_t kMaxFirstInteractionID = 10000;
|
||
|
||
constexpr uint32_t kNonPointerId = -1;
|
||
|
||
namespace mozilla::dom {
|
||
|
||
PerformanceInteractionMetrics::PerformanceInteractionMetrics() {
|
||
uint64_t randVal = RandomUint64().valueOr(kMinFirstInteractionID);
|
||
// Choose a random integer as the initial value to discourage developers from
|
||
// using interactionId to counter the number of interactions.
|
||
// https://wicg.github.io/event-timing/#user-interaction-value
|
||
mCurrentInteractionValue =
|
||
kMinFirstInteractionID +
|
||
(randVal % (kMaxFirstInteractionID - kMinFirstInteractionID + 1));
|
||
}
|
||
|
||
// https://w3c.github.io/event-timing/#sec-increasing-interaction-count
|
||
uint64_t PerformanceInteractionMetrics::IncreaseInteractionValueAndCount() {
|
||
mCurrentInteractionValue += kInteractionIdIncrement;
|
||
mInteractionCount++;
|
||
return mCurrentInteractionValue;
|
||
}
|
||
|
||
// https://w3c.github.io/event-timing/#sec-computing-interactionid
|
||
Maybe<uint64_t> PerformanceInteractionMetrics::ComputeInteractionId(
|
||
PerformanceEventTiming* aEventTiming, const WidgetEvent* aEvent) {
|
||
// Step 1. If event’s isTrusted attribute value is false, return 0.
|
||
if (!aEvent->IsTrusted()) {
|
||
return Some(0);
|
||
}
|
||
|
||
// Step 2. Let type be event’s type attribute value.
|
||
const EventMessage eventType = aEvent->mMessage;
|
||
|
||
// Step 3. If type is not one among keyup, compositionstart, input,
|
||
// pointercancel, pointerup, or click, return 0.
|
||
// Note: keydown and pointerdown are handled in finalize event timing.
|
||
switch (eventType) {
|
||
case eKeyDown:
|
||
case eKeyPress:
|
||
case eKeyUp:
|
||
case eCompositionStart:
|
||
case eEditorInput:
|
||
case ePointerCancel:
|
||
case ePointerDown:
|
||
case ePointerUp:
|
||
case eContextMenu:
|
||
case ePointerClick:
|
||
break;
|
||
default:
|
||
return Some(0);
|
||
}
|
||
|
||
// Step 4-8. Happens in the class constructor.
|
||
|
||
if (eventType == ePointerDown) {
|
||
uint32_t pointerId = aEvent->AsPointerEvent()->pointerId;
|
||
|
||
// If after the previously buffered pointerdown we haven't encountered any
|
||
// matching pointerup, we need to set it as zero so it properly flushes the
|
||
// events.
|
||
auto bufferedPointerdown = mPendingPointerDowns.MaybeGet(pointerId);
|
||
if (NS_WARN_IF(bufferedPointerdown &&
|
||
!(*bufferedPointerdown)->HasKnownInteractionId())) {
|
||
(*bufferedPointerdown)->SetInteractionId(0);
|
||
}
|
||
mPendingPointerDowns.InsertOrUpdate(pointerId, aEventTiming);
|
||
mContextMenuTriggered = false;
|
||
// InteractionId for this will be assigned by pointerup or pointercancel
|
||
// later.
|
||
return Nothing();
|
||
}
|
||
|
||
if (eventType == eKeyDown) {
|
||
const WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent();
|
||
|
||
if (keyEvent->mIsComposing) {
|
||
return Some(0);
|
||
}
|
||
|
||
auto code = keyEvent->mKeyCode;
|
||
|
||
// This is not part of the spec yet, but it's being discussed and will be
|
||
// added to the spec soon.
|
||
// See: https://github.com/w3c/event-timing/issues/153
|
||
MOZ_LOG(gEventTimingLog, mozilla::LogLevel::Debug,
|
||
("PendingKeyDowns: inserting keydown code=%u, total pending=%u",
|
||
code, mPendingKeyDowns.Count() + 1));
|
||
mPendingKeyDowns.InsertOrUpdate(code, aEventTiming);
|
||
uint64_t interactionId = IncreaseInteractionValueAndCount();
|
||
mLastKeydownInteractionValue = Some(interactionId);
|
||
MOZ_LOG(gEventTimingLog, mozilla::LogLevel::Debug,
|
||
("PendingKeyDowns: keydown assigned interactionId=%" PRIu64,
|
||
interactionId));
|
||
return Some(interactionId);
|
||
}
|
||
|
||
// keypress is not a part of the spec yet, but it's being worked on:
|
||
// https://github.com/w3c/event-timing/issues/149
|
||
if (eventType == eKeyPress) {
|
||
// Set the interaction id generated by the previous keydown entry.
|
||
return Some(mCurrentInteractionValue);
|
||
}
|
||
|
||
// Step 8. If type is keyup:
|
||
if (eventType == eKeyUp) {
|
||
// Step 8.1. If event’s isComposing attribute value is true, return 0.
|
||
const WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent();
|
||
if (keyEvent->mIsComposing) {
|
||
return Some(0);
|
||
}
|
||
|
||
// Step 8.2. Let code be event’s keyCode attribute value.
|
||
const uint32_t code = keyEvent->mKeyCode;
|
||
|
||
// Step 8.4. Let entry be pendingKeyDowns[code].
|
||
auto entry = mPendingKeyDowns.MaybeGet(code);
|
||
// Step 8.3. If pendingKeyDowns[code] does not exist, return 0.
|
||
if (!entry) {
|
||
MOZ_LOG(gEventTimingLog, mozilla::LogLevel::Debug,
|
||
("PendingKeyDowns: keyup code=%u not found in pendingKeyDowns "
|
||
"(size=%u), returning 0",
|
||
code, mPendingKeyDowns.Count()));
|
||
return Some(0);
|
||
}
|
||
|
||
uint64_t interactionId = (*entry)->InteractionId();
|
||
MOZ_LOG(gEventTimingLog, mozilla::LogLevel::Debug,
|
||
("PendingKeyDowns: keyup code=%u found, interactionId=%" PRIu64
|
||
", removing from pendingKeyDowns",
|
||
code, interactionId));
|
||
|
||
// Step 8.9. Remove pendingKeyDowns[code].
|
||
mPendingKeyDowns.Remove(code);
|
||
|
||
// Step 8.10. Return interactionId.
|
||
return Some(interactionId);
|
||
}
|
||
|
||
// Step 9. If type is compositionstart:
|
||
if (eventType == eCompositionStart) {
|
||
MOZ_LOG(gEventTimingLog, mozilla::LogLevel::Debug,
|
||
("PendingKeyDowns: compositionstart clearing %u pending keydowns",
|
||
mPendingKeyDowns.Count()));
|
||
// Step 9.1 For each entry in the values of pendingKeyDowns:
|
||
for (auto iter = mPendingKeyDowns.Iter(); !iter.Done(); iter.Next()) {
|
||
PerformanceEventTiming* entry = iter.Data();
|
||
// Step 9.1.1. Append entry to window’s entries to be queued.
|
||
entry->SetInteractionId(0);
|
||
}
|
||
|
||
// Step 9.2. Clear pendingKeyDowns.
|
||
mPendingKeyDowns.Clear();
|
||
// Step 9.3. Return 0
|
||
return Some(0);
|
||
}
|
||
|
||
// Step 10. If type is input:
|
||
if (eventType == eEditorInput) {
|
||
// Step 10.1. If event is not an instance of InputEvent, return 0.
|
||
const auto* inputEvent = aEvent->AsEditorInputEvent();
|
||
if (!inputEvent) {
|
||
return Some(0);
|
||
}
|
||
|
||
// Step 10.2. If event’s isComposing attribute value is false, return 0.
|
||
if (!inputEvent->mIsComposing) {
|
||
return Some(0);
|
||
}
|
||
|
||
mLastKeydownInteractionValue = Nothing();
|
||
return Some(IncreaseInteractionValueAndCount());
|
||
}
|
||
|
||
// Step 11. Otherwise (type is pointercancel, pointerup, or click):
|
||
|
||
MOZ_ASSERT(eventType == ePointerCancel || eventType == ePointerUp ||
|
||
eventType == ePointerClick || eventType == eContextMenu,
|
||
"Unexpected event type");
|
||
const auto* mouseEvent = aEvent->AsMouseEvent();
|
||
// Step 11.1. Let pointerId be event’s pointerId attribute value.
|
||
auto pointerId = mouseEvent->pointerId;
|
||
|
||
// Step 11.2. If type is click:
|
||
if (eventType == ePointerClick) {
|
||
if (pointerId == kNonPointerId) {
|
||
// -1 pointerId is a reserved value to indicate events that were generated
|
||
// by something other than a pointer device, like keydown.
|
||
// Return the interaction value of the keydown event instead.
|
||
return Some(mLastKeydownInteractionValue.valueOr(0));
|
||
}
|
||
|
||
// Step 11.2.2. Let value be pointerMap[pointerId].
|
||
auto value = mPointerInteractionValueMap.MaybeGet(pointerId);
|
||
// Step 11.2.1. If pointerMap[pointerId] does not exist, return 0.
|
||
if (!value) {
|
||
return Some(0);
|
||
}
|
||
|
||
// Step 11.2.3. Remove pointerMap[pointerId].
|
||
mPointerInteractionValueMap.Remove(pointerId);
|
||
// Step 11.2.4. Return value.
|
||
return Some(*value);
|
||
}
|
||
|
||
// Step 11.3. Assert that type is pointerup or pointercancel.
|
||
MOZ_RELEASE_ASSERT(eventType == ePointerUp || eventType == ePointerCancel ||
|
||
eventType == eContextMenu);
|
||
|
||
// Step 11.5. Let pointerDownEntry be pendingPointerDowns[pointerId].
|
||
auto entry = mPendingPointerDowns.MaybeGet(pointerId);
|
||
// Step 11.4. If pendingPointerDowns[pointerId] does not exist, return 0.
|
||
if (!entry) {
|
||
// This is the case where we have seen a pointerup before a contextmenu
|
||
// event. We return the same interactionId for the contextmenu.
|
||
// See https://github.com/w3c/event-timing/issues/155.
|
||
if (eventType == eContextMenu) {
|
||
return Some(mCurrentInteractionValue);
|
||
}
|
||
|
||
// This is the case where we have seen a contextmenu before a pointerup
|
||
// event. Similarly, we return the same interactionId, but also we reset the
|
||
// "is contextmenu triggered" flag to make sure that the next events are
|
||
// handled correctly. See https://github.com/w3c/event-timing/issues/155.
|
||
if (eventType == ePointerUp && mContextMenuTriggered) {
|
||
mContextMenuTriggered = false;
|
||
return Some(mCurrentInteractionValue);
|
||
}
|
||
return Some(0);
|
||
}
|
||
|
||
// Step 11.7. If type is pointerup:
|
||
if (eventType == ePointerUp || eventType == eContextMenu) {
|
||
// Step 11.7.1. Increase interaction count on window.
|
||
uint64_t interactionId = IncreaseInteractionValueAndCount();
|
||
|
||
// Step 11.7.2. Set pointerMap[pointerId] to window’s user interaction
|
||
// value.
|
||
mPointerInteractionValueMap.InsertOrUpdate(pointerId, interactionId);
|
||
|
||
// Step 11.7.3. Set pointerDownEntry’s interactionId to
|
||
// pointerMap[pointerId].
|
||
(*entry)->SetInteractionId(interactionId);
|
||
} else {
|
||
(*entry)->SetInteractionId(0);
|
||
}
|
||
|
||
// Step 11.9. Remove pendingPointerDowns[pointerId].
|
||
mPendingPointerDowns.Remove(pointerId);
|
||
|
||
if (eventType == eContextMenu) {
|
||
mContextMenuTriggered = true;
|
||
}
|
||
|
||
// Step 11.10. If type is pointercancel, return 0.
|
||
if (eventType == ePointerCancel) {
|
||
return Some(0);
|
||
}
|
||
|
||
return Some(mPointerInteractionValueMap.Get(pointerId));
|
||
}
|
||
|
||
} // namespace mozilla::dom
|